Questionnaire

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.

Question 1 of 2
What best describes your role?
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.json

Usage

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.

Choose a plan

Press A or B to answer with the keyboard.

<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.

What stood out to you?
<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

PropTypeDefaultDescription
itemsQuestionnaireItemDefinition[]–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.
defaultItemstring–Name of the step shown first (uncontrolled).
itemstring–Controlled active step name.
onItemChange(item: string) => void–Fires when the active step changes.
onSubmitFormEventHandler–Standard form submit; every answered step contributes its FormData entries. Invalid steps cancel submission and receive focus.

QuestionnaireItem

PropTypeDefaultDescription
namestring–Form field name; must match an items definition.
requiredbooleanfalseBlocks Next/Submit until answered and hides the Skip action.
multiplebooleanfalseCheckbox choices instead of radios.
invalidbooleanfalseMarks the step invalid from consumer validation.
onStatusChange(status) => void–Reports "unanswered" | "answered" | "skipped" transitions.
ComponentDescription
QuestionnaireProgressLive "Question X of Y" indicator (data-slot="questionnaire-progress") with progressbar semantics.
QuestionnaireTitle / QuestionnaireDescriptionStep heading (a fieldset legend) and supporting copy wired to aria-describedby.
QuestionnaireChoices / QuestionnaireChoiceChoice list and card-style rows (data-slot="questionnaire-choice") with the espresso radio/checkbox indicator, elevation-aware background, and an invisible full-cover input.
QuestionnaireChoiceDescriptionMuted second line inside a choice label.
QuestionnaireInputFree-text answer (text, email, number, date…); filled state feeds validation.
QuestionnaireErrorValidation message, auto-worded from required-ness and shown only while invalid.
Previous / Skip / Next / SubmitEspresso 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.

Search documentation

Search the component docs