AI Patterns

Clarifying QuestionsNew

A short card of questions before an ambiguous task, each pre-filled with the assistant's suggested answer, that collapses into a summary once started.

JobStart
Seen in
Make a deck summarising our Q3 results

A few quick questions so I get this right. I've filled in my best guesses — change anything that's off.

1. Who is it for?
2. How long?
3. Tone
Using my suggestions
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Check, Pencil } from "lucide-react";

import { cn } from "@/lib/utils";

export interface ClarifyingQuestion {
  id: string;
  label: string;
  options: string[];
  /** The assistant's best guess, pre-selected and marked "Suggested". */
  suggested?: string;
  /** Adds a free-text "Other" option. */
  allowOther?: boolean;
}

export type ClarifyingAnswers = Record<string, string>;

export interface ClarifyingQuestionsProps {
  intro?: string;
  questions: ClarifyingQuestion[];
  /** Called with every answer — the suggested ones included — when the person starts. */
  onSubmit: (answers: ClarifyingAnswers, edited: string[]) => void;
  submitLabel?: string;
  className?: string;
}

const OTHER = "__other__";

export function ClarifyingQuestions({
  intro = "A few quick questions so I get this right. I've filled in my best guesses — change anything that's off.",
  questions,
  onSubmit,
  submitLabel = "Start",
  className,
}: ClarifyingQuestionsProps) {
  const initial = React.useMemo(
    () => Object.fromEntries(questions.map((q) => [q.id, q.suggested ?? ""])) as ClarifyingAnswers,
    [questions]
  );
  const [answers, setAnswers] = React.useState<ClarifyingAnswers>(initial);
  const [other, setOther] = React.useState<ClarifyingAnswers>({});
  const [submitted, setSubmitted] = React.useState(false);
  const id = React.useId();

  const resolved = (q: ClarifyingQuestion) => (answers[q.id] === OTHER ? other[q.id]?.trim() ?? "" : answers[q.id]);
  const missing = questions.filter((q) => !resolved(q));
  const edited = questions.filter((q) => resolved(q) !== (q.suggested ?? "")).map((q) => q.id);

  function submit() {
    if (missing.length) return;
    setSubmitted(true);
    onSubmit(Object.fromEntries(questions.map((q) => [q.id, resolved(q)])), edited);
  }

  return (
    <div className={cn("w-full rounded-2xl border bg-card", className)}>
      <AnimatePresence mode="wait" initial={false}>
        {submitted ? (
          <motion.div
            key="summary"
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.2, ease: "easeOut" }}
            className="overflow-hidden"
          >
            <div role="status" className="flex items-start gap-3 px-4 py-3">
              <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-background">
                <Check className="size-3" aria-hidden />
              </span>
              <div className="min-w-0 flex-1">
                <p className="text-sm font-medium">Got it — starting with these details</p>
                <ul className="mt-1.5 flex flex-wrap gap-1.5">
                  {questions.map((q) => (
                    <li key={q.id} className="rounded-full border bg-muted/50 px-2 py-0.5 text-xs">
                      <span className="text-muted-foreground">{q.label}: </span>
                      {resolved(q)}
                    </li>
                  ))}
                </ul>
              </div>
              <button
                type="button"
                onClick={() => setSubmitted(false)}
                aria-label="Change answers"
                title="Change answers"
                className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
              >
                <Pencil className="size-3.5" aria-hidden />
              </button>
            </div>
          </motion.div>
        ) : (
          <motion.form
            key="form"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.15 }}
            onSubmit={(e) => {
              e.preventDefault();
              submit();
            }}
            className="flex flex-col gap-4 p-4"
          >
            <p className="text-sm text-pretty">{intro}</p>

            {questions.map((q, qi) => (
              <fieldset key={q.id} className="flex flex-col gap-2">
                <legend className="mb-2 text-xs font-medium text-muted-foreground">
                  {qi + 1}. {q.label}
                </legend>
                <div className="flex flex-wrap gap-1.5">
                  {[...q.options, ...(q.allowOther ? [OTHER] : [])].map((option) => {
                    const checked = answers[q.id] === option;
                    return (
                      <label key={option} className="relative">
                        <input
                          type="radio"
                          name={`${id}-${q.id}`}
                          value={option}
                          checked={checked}
                          onChange={() => setAnswers((a) => ({ ...a, [q.id]: option }))}
                          className="peer sr-only"
                        />
                        <span
                          className={cn(
                            "inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring",
                            checked
                              ? "border-foreground bg-foreground text-background"
                              : "bg-background text-foreground hover:bg-accent"
                          )}
                        >
                          {option === OTHER ? "Other…" : option}
                          {option === q.suggested && (
                            <span className={cn("text-[10px]", checked ? "text-background/70" : "text-muted-foreground")}>
                              Suggested
                            </span>
                          )}
                        </span>
                      </label>
                    );
                  })}
                </div>
                <AnimatePresence initial={false}>
                  {answers[q.id] === OTHER && (
                    <motion.input
                      initial={{ opacity: 0, height: 0 }}
                      animate={{ opacity: 1, height: "auto" }}
                      exit={{ opacity: 0, height: 0 }}
                      autoFocus
                      value={other[q.id] ?? ""}
                      onChange={(e) => setOther((o) => ({ ...o, [q.id]: e.target.value }))}
                      aria-label={`${q.label}, other`}
                      placeholder="Type your answer"
                      className="w-full rounded-lg border bg-background px-3 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
                    />
                  )}
                </AnimatePresence>
              </fieldset>
            ))}

            <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
              <span aria-live="polite" className="text-xs text-muted-foreground">
                {missing.length
                  ? `${missing.length} still to answer`
                  : edited.length
                    ? `${edited.length} changed from my suggestions`
                    : "Using my suggestions"}
              </span>
              <button
                type="submit"
                disabled={missing.length > 0}
                className="rounded-xl bg-foreground px-4 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
              >
                {submitLabel}
              </button>
            </div>
          </motion.form>
        )}
      </AnimatePresence>
    </div>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Clarifying Questions

## Summary
Before starting on an ambiguous request, the assistant replies with a short card of two or three questions, all visible at once, each pre-filled with its best guess and marked "Suggested". The person corrects only what's wrong and presses Start. The card then collapses into a summary of the details the assistant will use.

## When to use
- Requests where a wrong assumption is expensive: a deck, a report, a research run, a long generation.
- Requests that are clear in intent but missing two or three details that change the result (audience, length, tone, scope).
- When the assistant can make sensible guesses — pre-filling them turns a quiz into a quick check.

## When not to use
- When the request is already specific, or a wrong guess is cheap to fix afterwards. Just answer, and let Refinement Controls handle tweaks.
- Mid-task decisions that block an agent — use Human in the Loop Question, which pauses the run and walks through one choice at a time.
- More than three or four questions. Ask for the essentials, or break the work into a planning step (Plan Review).
- Confirming a specific risky action — use Tool Approval.

## Anatomy
- Intro line: why the assistant is asking, and that it has filled in guesses.
- Question groups: a numbered label and a row of option chips; the suggested option is pre-selected and tagged "Suggested".
- Optional "Other…" chip that reveals a text field.
- Footer: a live status ("Using my suggestions", "1 changed from my suggestions", "1 still to answer") and the Start button.
- Summary: after Start, a compact row of the chosen details as chips, with an edit button to reopen the questions.

## Behavior
- All questions show at once, so people can scan and change only what matters; there is no step-by-step flow.
- Questions with a suggestion are answered from the start, so Start works immediately. Questions without one block Start until answered.
- Choosing "Other…" reveals and focuses a text field; an empty one counts as unanswered.
- Start submits every answer and reports which ones the person changed, so the assistant can weight them.
- The card collapses to the summary and the assistant begins. The edit button reopens the questions with the previous answers.

## Content guidelines
- Ask only what changes the output. Each question should be answerable with a tap.
- Keep options to two to four short, mutually exclusive labels; put the most likely one first or mark it as suggested.
- Phrase the intro as help, not a gate: "A few quick questions so I get this right."
- The summary repeats the details in the user's words, so mistakes are easy to spot.

## Accessibility
- Each question is a `<fieldset>` with a `<legend>`; options are native radio inputs styled as chips, so arrow keys move between them and screen readers announce the group.
- Chips show a visible focus ring; the selected state is shown by fill and announced by the radio.
- The footer status is `aria-live="polite"` and the summary is `role="status"`.
- The edit button has an accessible name ("Change answers").

## Related patterns
- **Human in the Loop Question** — Use it instead when an agent is mid-run and must pause for one decision at a time.
- **Prompt Enhancer** — Use it instead when filling in the details silently is acceptable and the person can undo.
- **Plan Review** — Use it as well for longer tasks: clarify the details first, then confirm the plan.
- **Follow-Up List** — Use it instead to suggest next questions after an answer, not to gather input before one.

Do’s and don’ts

Do use it for

  • Requests where a wrong assumption is expensive: a deck, a report, a research run, a long generation.
  • Requests that are clear in intent but missing two or three details that change the result (audience, length, tone, scope).
  • When the assistant can make sensible guesses — pre-filling them turns a quiz into a quick check.

Don’t use it for

  • When the request is already specific, or a wrong guess is cheap to fix afterwards. Just answer, and let Refinement Controls handle tweaks.
  • Mid-task decisions that block an agent — use Human in the Loop Question, which pauses the run and walks through one choice at a time.
  • More than three or four questions. Ask for the essentials, or break the work into a planning step (Plan Review).
  • Confirming a specific risky action — use Tool Approval.