Questionnaire
A multi-step form flow — one question at a time with choices or free text, keyboard shortcuts, validation and skip logic. Built on the @shadcn/react questionnaire primitive.
Preview
The root renders a form and shows one QuestionnaireItem at a time. Enter advances, ⌘Enter submits from any step, arrow keys move between questions and answers, and required items block Next until answered. Choice surfaces sit on bg-card and follow the elevation ladder inside cards, dialogs and popovers.
const items = [
{
name: "role",
required: true,
choices: [{ value: "designer" }, { value: "engineer" }, { value: "other" }],
},
{ name: "email" },
] as const
<Questionnaire items={items} onSubmit={handleSubmit}>
<QuestionnaireProgress />
<QuestionnaireItem name="role" required>
<QuestionnaireTitle>What best describes your role?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="designer">Designer</QuestionnaireChoice>
<QuestionnaireChoice value="engineer">Engineer</QuestionnaireChoice>
<QuestionnaireChoice value="other">Other</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="email">
<QuestionnaireTitle>Where can we reach you?</QuestionnaireTitle>
<QuestionnaireInput type="email" placeholder="you@example.com" />
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext />
<QuestionnaireSubmit />
</QuestionnaireActions>
</Questionnaire>Installation
npx shadcn@latest add https://espresso-base-ui.vercel.app/r/questionnaire.jsonUsage
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"Declare the flow once in items — the same names, choices, required and disabled flags as the rendered markup. The definition drives keyboard shortcuts and validation, and the primitive warns in development when the two drift apart.
Keyboard shortcuts
shortcuts="letters" (or "numbers") assigns each enabled choice a key, shown as a badge at the end of the row. Pressing the key selects the choice; disabled choices are skipped when assigning.
<Questionnaire items={items} shortcuts="letters" onSubmit={handleSubmit}>
<QuestionnaireItem name="plan" required>
<QuestionnaireTitle>Choose a plan</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="hobby">
Hobby
<QuestionnaireChoiceDescription>
For personal projects
</QuestionnaireChoiceDescription>
</QuestionnaireChoice>
{/* … */}
</QuestionnaireChoices>
</QuestionnaireItem>
</Questionnaire>Multiple selection
multiple on an item renders its choices as checkboxes instead of radios. Items without required show the Skip action automatically.
<QuestionnaireItem name="highlights" multiple>
<QuestionnaireTitle>What stood out to you?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="performance" defaultChecked>
Performance
</QuestionnaireChoice>
<QuestionnaireChoice value="design">Design</QuestionnaireChoice>
<QuestionnaireChoice value="docs">Documentation</QuestionnaireChoice>
</QuestionnaireChoices>
</QuestionnaireItem>API reference
Questionnaire
| Prop | Type | Default | Description |
|---|---|---|---|
| items | QuestionnaireItemDefinition[] | – | Declarative flow definition: { name, required?, disabled?, choices? } per step. Drives shortcuts, validation and dev-time consistency warnings. |
| shortcuts | "letters" | "numbers" | – | Assigns keyboard shortcuts to choices and shows the badge on each row. |
| defaultItem | string | – | Name of the step shown first (uncontrolled). |
| item | string | – | Controlled active step name. |
| onItemChange | (item: string) => void | – | Fires when the active step changes. |
| onSubmit | FormEventHandler | – | Standard form submit; every answered step contributes its FormData entries. Invalid steps cancel submission and receive focus. |
QuestionnaireItem
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | – | Form field name; must match an items definition. |
| required | boolean | false | Blocks Next/Submit until answered and hides the Skip action. |
| multiple | boolean | false | Checkbox choices instead of radios. |
| invalid | boolean | false | Marks the step invalid from consumer validation. |
| onStatusChange | (status) => void | – | Reports "unanswered" | "answered" | "skipped" transitions. |
| Component | Description |
|---|---|
| QuestionnaireProgress | Live "Question X of Y" indicator (data-slot="questionnaire-progress") with progressbar semantics. |
| QuestionnaireTitle / QuestionnaireDescription | Step heading (a fieldset legend) and supporting copy wired to aria-describedby. |
| QuestionnaireChoices / QuestionnaireChoice | Choice list and card-style rows (data-slot="questionnaire-choice") with the espresso radio/checkbox indicator, elevation-aware background, and an invisible full-cover input. |
| QuestionnaireChoiceDescription | Muted second line inside a choice label. |
| QuestionnaireInput | Free-text answer (text, email, number, date…); filled state feeds validation. |
| QuestionnaireError | Validation message, auto-worded from required-ness and shown only while invalid. |
| Previous / Skip / Next / Submit | Espresso Buttons that show and hide themselves: Previous after step one, Skip on optional steps, Next until the last step, Submit on it. |
Keyboard
Enter advances from an answered step, ⌘/Ctrl+Enter submits from anywhere, ArrowUp/ArrowDown move between answers, ArrowLeft/ArrowRight switch steps, and letter or number keys select choices when shortcuts is on. Every control carries aria-keyshortcuts, steps render as fieldsets with legends, and inactive steps are inert and hidden.