Questionnaire

A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions.

Loading example…

Installation

bunx --bun shadcn@latest add @sui/questionnaire

Install with the shadcn CLI or use the shared @workspace/ui package. Follow the installation guide to configure the registry, load styles, and choose import aliases.

Usage

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@workspace/ui/components/questionnaire"
const items = [
  {
    name: "direction",
    required: true,
    prompt: "What should we prototype next?",
    description: "Choose a direction or write your own.",
    choices: [
      {
        value: "delegation",
        label: "Delegation",
        description: "Show how work moves to a specialist.",
      },
      {
        value: "questions",
        label: "Question prompts",
        description: "Show choices while the interface waits.",
      },
      { value: "both", label: "Both together" },
    ],
    input: { label: "Another answer", placeholder: "Type another answer…" },
  },
  {
    name: "detail",
    required: false,
    prompt: "How much detail should it include?",
    description: "Skip this if you are not sure yet.",
    choices: [
      { value: "focused", label: "Focused" },
      { value: "complete", label: "Complete flow" },
    ],
  },
] as const

Define the collection once: pass it to Questionnaire for server-rendered progress, actions, and shortcuts, then map it into the parts.

<Questionnaire items={items} onSubmit={handleSubmit}>
  <QuestionnaireProgress />
  {items.map((question) => (
    <QuestionnaireItem
      key={question.name}
      name={question.name}
      required={question.required}
    >
      <QuestionnaireTitle>{question.prompt}</QuestionnaireTitle>
      <QuestionnaireDescription>
        {question.description}
      </QuestionnaireDescription>
      <QuestionnaireChoices>
        {question.choices.map((choice) => (
          <QuestionnaireChoice key={choice.value} value={choice.value}>
            <span className="font-medium">{choice.label}</span>
            {"description" in choice ? (
              <span className="text-muted-foreground">
                {choice.description}
              </span>
            ) : null}
          </QuestionnaireChoice>
        ))}
        {"input" in question ? (
          <QuestionnaireInput
            aria-label={question.input.label}
            placeholder={question.input.placeholder}
          />
        ) : null}
      </QuestionnaireChoices>
      <QuestionnaireError />
    </QuestionnaireItem>
  ))}
  <QuestionnaireActions>
    <QuestionnairePrevious />
    <QuestionnaireSkip />
    <QuestionnaireNext />
    <QuestionnaireSubmit />
  </QuestionnaireActions>
</Questionnaire>
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault()
  const answers = new FormData(event.currentTarget)
  // answers.get("direction"), answers.getAll(...) for multiple items.
}

Composition

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

Questionnaire owns the ordered items, active item, answer state, validation, progress, and navigation. The containing page, card, dialog, or drawer owns close and cancellation behavior, persistence, transport, and branching.

Server Rendering

Pass items to server-render the active item, progress, actions, and answer shortcuts. See the headless Questionnaire for the complete behavior.

Multiple Selection

Use multiple for an item that accepts more than one fixed answer.

Loading example…

Freeform Answer

Compose QuestionnaireInput with fixed choices when the user can provide another answer.

Loading example…

Explicit Skip

Add QuestionnaireSkip when an optional item may be intentionally left unanswered.

Loading example…

Shortcuts

Assign a letter or number key to each answer with shortcuts.

Loading example…

Custom Validation

Combine controlled navigation with an external schema such as Zod to return to an invalid item and present its error.

Loading example…

Controlled

Control the active item from host state, such as returning to an invalid step.

Loading example…

Resume

Restore a saved active item and default answers, then reset changes back to that saved state.

Loading example…

Conditional Items

Disable items that do not apply to the user's earlier answers.

Loading example…

Read item status to opt into disabled navigation and custom action styling.

Loading example…

Custom Progress

Use the Progress render state to build a custom progress indicator.

Loading example…

Animated Items

Animate the active item while keeping progress and navigation stationary.

Loading example…

Card

Compose Questionnaire with Card slots while keeping the question title and description semantic.

Loading example…

Dialog

Compose Questionnaire inside a Dialog while keeping cancellation and dismissal host-owned.

Loading example…

Accessibility

QuestionnaireItem renders a fieldset, and QuestionnaireTitle renders its legend. Descriptions and active errors are associated with the current item, and invalid items and answer controls expose aria-invalid.

Fixed choices preserve native radio and checkbox behavior. Progress is exposed as a named progressbar, navigation uses real buttons, and inactive items and actions are hidden and inert. Successful navigation focuses the newly active item; failed validation focuses an available answer control.

Always give QuestionnaireInput an accessible name with a visible label, aria-label, or aria-labelledby. A placeholder is not a label. See the Questionnaire accessibility guide for labeling custom compositions and the complete keyboard behavior.

Unstyled

The behavior in Questionnaire comes from the @shadcn/react package. To use it directly with your own markup and styles, see Questionnaire under @shadcn/react.

API Reference

The props, data attributes, and render states for every part are documented on the @shadcn/react Questionnaire page. The styled components inherit the corresponding unstyled props. Navigation components also accept Button size and variant props, and QuestionnaireActions is a styled-only layout helper.