Questionnaire

Questions the model asks one at a time, with fixed choices, a freeform answer and skips.

View as Markdown
Question 1 of 3
What should the agent build next?

Choose a direction or describe another task.

<Questionnaire items={items} onSubmit={handleSubmit} shortcuts="letters">  <QuestionnaireProgress />  {items.map((question) => (    <QuestionnaireItem      key={question.name}      multiple={question.multiple}      name={question.name}      required={question.required}    >      <QuestionnaireTitle>{question.title}</QuestionnaireTitle>      <QuestionnaireDescription>        {question.description}      </QuestionnaireDescription>      <QuestionnaireChoices>        {question.choices.map((choice) => (          <QuestionnaireChoice key={choice.value} value={choice.value}>            {choice.label}            <QuestionnaireChoiceDescription>              {choice.description}            </QuestionnaireChoiceDescription>          </QuestionnaireChoice>        ))}      </QuestionnaireChoices>      <QuestionnaireError />    </QuestionnaireItem>  ))}  <QuestionnaireActions>    <QuestionnairePrevious />    <QuestionnaireSkip />    <QuestionnaireNext />    <QuestionnaireSubmit>Save plan</QuestionnaireSubmit>  </QuestionnaireActions></Questionnaire>

Installation

$ npx shadcn@latest add @constella/questionnaire

Patch @shadcn/react 0.3.1 if you server-render. On React 19.3, a question with a QuestionnaireInput loops on hydration ("Maximum update depth exceeded"); apply Constella's patch with pnpm patch until a fixed release ships.

Usage

import {  Questionnaire,  QuestionnaireActions,  QuestionnaireChoice,  QuestionnaireChoiceDescription,  QuestionnaireChoices,  QuestionnaireDescription,  QuestionnaireError,  QuestionnaireInput,  QuestionnaireItem,  QuestionnaireNext,  QuestionnairePrevious,  QuestionnaireProgress,  QuestionnaireSkip,  QuestionnaireSubmit,  QuestionnaireTitle,} from "@/components/ai/questionnaire";
const items = [  {    name: "direction",    required: true,    choices: [{ value: "delegation" }, { value: "questions" }],  },  { name: "detail", choices: [{ value: "focused" }, { value: "complete" }] },] as const;

Define the questions once: pass them to Questionnaire as items so progress, actions and shortcuts render on the server, then map them into the parts. The answers arrive as the form's data on submit, one entry per name; getAll reads a multiple question.

function handleSubmit(event: SubmitEvent<HTMLFormElement>) {  event.preventDefault();  const answers = new FormData(event.currentTarget);}

Composition

Questionnaire├── QuestionnaireProgress├── QuestionnaireItem│   ├── QuestionnaireTitle│   ├── QuestionnaireDescription│   ├── QuestionnaireChoices│   │   ├── QuestionnaireChoice│   │   │   └── QuestionnaireChoiceDescription│   │   └── QuestionnaireInput│   └── QuestionnaireError└── QuestionnaireActions    ├── QuestionnairePrevious    ├── QuestionnaireSkip    ├── QuestionnaireNext    └── QuestionnaireSubmit

Questionnaire owns the order, the active question, the answers, validation and navigation. The page, card or dialog around it owns closing, saving, sending and branching.

Multiple

Use the multiple prop on an item that takes more than one answer. Its choices become checkboxes.

What context should the agent inspect?

Select every source that may affect the implementation.

<QuestionnaireItem multiple name="context" required>  <QuestionnaireTitle>    What context should the agent inspect?  </QuestionnaireTitle>  <QuestionnaireChoices>    <QuestionnaireChoice value="source">      Relevant source files    </QuestionnaireChoice>    <QuestionnaireChoice value="tests">Existing tests</QuestionnaireChoice>  </QuestionnaireChoices>  <QuestionnaireError /></QuestionnaireItem>

Questionnaire Input

Add a QuestionnaireInput after the choices for an answer of the user's own. Typing in it clears the picked choice. Give it an aria-label; a placeholder is not a label.

How should the agent approach this refactor?

Choose a strategy or write a more specific instruction.

<QuestionnaireChoices>  <QuestionnaireChoice value="incremental">    Make the smallest safe change  </QuestionnaireChoice>  <QuestionnaireInput    aria-label="Another refactoring approach"    placeholder="Describe another approach…"  /></QuestionnaireChoices>

Questionnaire Skip

Add a QuestionnaireSkip to let an optional item be left unanswered on purpose. It shows only on items without required; onStatusChange tells skipped apart from unanswered.

Question 1 of 3
What kind of change is this?

Choose the category that best describes the work.

<QuestionnaireItem name="constraints" onStatusChange={setStatus}>  …</QuestionnaireItem><QuestionnaireActions>  <QuestionnairePrevious />  <QuestionnaireSkip />  <QuestionnaireNext />  <QuestionnaireSubmit /></QuestionnaireActions>

Shortcuts

Use the shortcuts prop to give each answer a letter or number key, shown as a Kbd on the choice. Enter moves on, and arrow keys move between answers and questions.

What should the agent do next?

Press the key shown on an answer, or move with the arrow keys.

<Questionnaire items={items} shortcuts="numbers">  …</Questionnaire>

Questionnaire Progress

Use the render prop for your own progress, from state.current and state.total. A Progress bar inside it is aria-hidden, since the part already reports the step.

How large is the change?
<QuestionnaireProgress  render={(props, state) => (    <div {...props}>      <Progress aria-hidden max={state.total} value={state.current}>        <ProgressLabel>          Step {state.current} of {state.total}        </ProgressLabel>      </Progress>    </div>  )}/>

Reset

Use defaultItem and defaultChecked to resume a saved draft. A type="reset" button restores those answers and returns to the saved question.

Question 2 of 3
How should the migration be verified?

These checks were selected during the previous session.

<Questionnaire defaultItem="verification" items={items}>  <QuestionnaireItem name="change" required>    <QuestionnaireChoice defaultChecked value="incremental">      Incremental migration    </QuestionnaireChoice>  </QuestionnaireItem>  …  <QuestionnaireActions>    <Button type="reset" variant="outline">      Reset changes    </Button>    <QuestionnairePrevious />    <QuestionnaireNext />    <QuestionnaireSubmit />  </QuestionnaireActions></Questionnaire>

Controlled

Use item and onItemChange to hold the active question in your own state.

Question 1 of 3
What may the agent change?

The page holds the active question; the controls move it.

const [item, setItem] = useState("scope");
<Questionnaire item={item} items={items} onItemChange={setItem}>  …</Questionnaire>;

Disabled

Questionnaire Item

Use the disabled prop on an item, and on its entry in items, to drop a question that doesn't apply. Progress and navigation skip it.

Question 1 of 2
Where should the agent run?

Cloud runs add an environment question to this flow.

const items = [  { name: "runtime", required: true },  { disabled: runtime !== "cloud", name: "environment", required: true },];
<QuestionnaireItem disabled={runtime !== "cloud"} name="environment" required>  …</QuestionnaireItem>;

Questionnaire Next

Read each item's status with onStatusChange to disable Next until it has an answer. By default Next stays enabled and shows the error instead.

Question 1 of 2
What may the agent modify?

Next stays disabled until an answer is chosen.

<QuestionnaireNext disabled={unanswered} variant="secondary" /><QuestionnaireSubmit disabled={unanswered} />

Invalid

Use the invalid prop with your own schema, such as Zod, and pass the message to QuestionnaireError. Move to the invalid question with item.

How much detail should the answer include?

Choose the response depth.

1 / 2
<QuestionnaireItem invalid={errors.detail !== undefined} name="detail" required>  …<QuestionnaireError>{errors.detail}</QuestionnaireError></QuestionnaireItem>

Card

Wrap the items in a Card, each question's title in its CardHeader and the actions in its CardFooter. Point the item's aria-labelledby at the title, since it no longer sits first in the fieldset.

What should the agent work on?

Choose the task that should be handled next.

Question 1 of 2
<Questionnaire items={items}>  <Card>    <QuestionnaireItem aria-labelledby={titleId} name="task" required>      <CardHeader>        <QuestionnaireTitle id={titleId}>          What should the agent work on?        </QuestionnaireTitle>        <CardAction>          <QuestionnaireProgress />        </CardAction>      </CardHeader>      <CardContent>…</CardContent>    </QuestionnaireItem>    <CardFooter>      <QuestionnaireActions>…</QuestionnaireActions>    </CardFooter>  </Card></Questionnaire>

Dialog

Put the questionnaire in a Dialog and close it from onSubmit. Cancel is a DialogClose, not part of the questionnaire.

<DialogContent aria-label="Clarification">  <Questionnaire items={items} onSubmit={handleSubmit}>    …    <DialogFooter>      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>      <QuestionnaireActions>…</QuestionnaireActions>    </DialogFooter>  </Questionnaire></DialogContent>

API Reference

Every part takes sx, applied last. The rest is shadcn's Questionnaire.

PartAdds
QuestionnaireChoiceA card holding the radio or checkbox, label and shortcut
QuestionnaireChoiceDescriptionA secondary line under the choice's label
QuestionnaireActionsA row: Previous at the start, the rest at the end
QuestionnairePrevious, SkipButton's size and variant; outline by default
QuestionnaireNext, SubmitButton's size and variant; primary by default