shadcn.io is an independent project built on shadcn/ui
The current question is the only one on screen. The next one stays hidden until the person gets there.
Use it when people should answer in order, and a later question only makes sense after an earlier one. A short settings page is stacked Field groups instead. In React, Ark UI renders each question as a fieldset, so the title is the legend.
Installation
Anatomy

Questionnaire
├── QuestionnaireProgress
├── QuestionnaireItem
│ ├── QuestionnaireTitle
│ ├── QuestionnaireDescription
│ ├── QuestionnaireChoices
│ │ ├── QuestionnaireChoice
│ │ └── QuestionnaireInput
│ └── QuestionnaireError
└── QuestionnaireActions
├── QuestionnairePrevious
├── QuestionnaireSkip
├── QuestionnaireNext
└── QuestionnaireSubmitUsage
import type { FormEvent } from "react";
import {
Questionnaire, QuestionnaireActions, QuestionnaireChoice,
QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError,
QuestionnaireInput, QuestionnaireItem, QuestionnaireNext,
QuestionnairePrevious, QuestionnaireProgress, QuestionnaireSkip,
QuestionnaireSubmit, QuestionnaireTitle,
} from "@/components/ui/questionnaire";const items = [
{ choices: [{ value: "delegation" }], name: "direction", required: true },
] as const;
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const answers = new FormData(event.currentTarget);
console.log(answers.get("direction"));
}<Questionnaire items={items} onSubmit={handleSubmit} className="max-w-full min-w-0">
<QuestionnaireProgress />
<QuestionnaireItem name="direction" required>
<QuestionnaireTitle>What should we prototype next?</QuestionnaireTitle>
<QuestionnaireDescription>Choose a direction.</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="delegation">Delegation</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext />
<QuestionnaireSubmit />
</QuestionnaireActions>
</Questionnaire>Demos
Multiple
multiple on the item turns the question into checkboxes, and the submit carries every checked value under one name. That fits a question like what to inspect, when more than one source is an honest answer. Leave the prop off when only one answer is valid, or people will tick a combination you cannot act on. Read the list back with getAll, since get returns only the first value.
Freeform
QuestionnaireInput sits in the choice list so someone can type an answer the options missed. Use it when the list is a suggestion, a strategy or a feature name, not a closed set. The typed text submits under the same name as a chosen option. If an empty custom answer is not a real answer, keep the item required.
Skip
QuestionnaireSkip stays available on an optional item and hides itself when the item is required. The constraints question can be passed, and onStatusChange reports skipped, which is not the same as never opening it. Use skip for a useful question that must not block the flow. Do not offer it on a question a later step will need.
Shortcuts
shortcuts accepts letters or numbers, and each choice grows a key. The select above the question switches that mode, including off. Letters suit an internal tool where people live on the keyboard. Leave them off for a survey that will mostly be tapped, and use Cmd or Ctrl with Enter to move forward.
Validation
required already holds Next until there is an answer, and QuestionnaireError can say so. This flow goes further: the two answers are checked together, and a public summary is rejected. item with onItemChange jumps back to the first bad question, and invalid marks it. Use that when one answer makes another illegal, not for a simple missing choice.
Controlled
item and onItemChange keep the current question name in state, and the line above the form only prints that name. Take control when something outside the questionnaire has to know where the person is, or when submit should send them back to a specific question. defaultItem is enough if you only choose where it opens.
Resume
defaultItem reopens on a later question, and defaultChecked plus defaultValue put the saved answers back. The reset button fires onReset, so a restored draft can be told apart from edits made just now. Use this when someone left a flow halfway and should not restart at the first question. Owning the current item the whole time is the controlled demo, not this one.
Conditional
The environment question stays disabled until Cloud is chosen, on the item and in the items list, so a local run never stops there. Use disabled when a follow-up only makes sense for one answer, and keep the choice in state so the list can change as they click. A question you always ask should stay enabled, even if you wish it were shorter.
Navigation State
onStatusChange reports the current item as unanswered, and Next plus Submit set disabled from that. The opacity class tied to unanswered status makes the blocked button look blocked, not merely silent. Use this when the action should look unavailable until a choice exists. required already refuses to advance, and this is the visible version of that rule.
Progress
QuestionnaireProgress can take a render function that receives the element props plus current and total. The bar here draws one segment per question and writes the count beside it. Use a custom render when the stock progress text is too quiet, or when that count has to live in a card action. The plain progress is enough on a short flow.
Animated
Each item opts into a short fade and a rise while data-active, and motion-reduce turns that off. A little motion helps when a hard swap between questions feels like a glitch, but a long travel on every Next becomes the thing people wait on. Put the classes on the item, not the root, because the root does not enter and leave.
Card
The questions live in a Card. The title passes render so it becomes a CardTitle, the description becomes a CardDescription, and progress sits in CardAction, with the buttons in the footer. Use a card when this flow is the page and should share a surface with the rest of the UI. A bare questionnaire is lighter when it already sits inside some other panel.
Dialog
The flow opens inside a Dialog from a button, and a successful submit closes it. The question title is the dialog title, so the modal keeps a name instead of an empty one. Use a dialog when the questions are a side task, such as a clarification, and the page should stay put. A card on the page fits better when the flow is why anyone opened it.
FAQ
When should I use a Questionnaire instead of a regular form?
Use a Questionnaire when people should answer one question at a time: onboarding, surveys, quizzes, or flows where the next question depends on the last answer. A regular form built with Field shows every field at once, which suits short forms and settings pages better.
How do I get the answers from a Questionnaire?
Questionnaire is a form, so handle onSubmit and read the answers with FormData, by each item's name. For an item with multiple, use formData.getAll(name), since get returns only the first answer; a skipped question submits no value at all.
How do I make a Questionnaire question required?
Set required on its QuestionnaireItem, and on the same item in items. Next and Submit then wait for an answer, QuestionnaireError says "Choose an answer to continue.", and QuestionnaireSkip hides itself on that question. The Validation demo above shows it.
How do I control which question the Ark UI Questionnaire shows?
Pass item, the current question's name, and onItemChange on Questionnaire, as in the Controlled demo. To only choose where it starts, pass defaultItem: the Resume demo uses it to reopen a saved questionnaire at the question people left off.
Does the Questionnaire support keyboard shortcuts?
Yes. Set shortcuts to letters or numbers, and each choice gets a key that picks it. Cmd or Ctrl with Enter moves to the next question, ArrowUp and ArrowDown move between answers, and ArrowLeft goes back, as the Shortcuts demo shows.
Is the Ark UI Questionnaire accessible?
Yes. Each question is a fieldset with its title as the legend, and only the active one can be reached, since the others are hidden and inert. QuestionnaireProgress is a progress bar that announces "Question 2 of 5", and errors are announced as alerts.
API Reference
Questionnaire
<form> data-slot="questionnaire"
item?string
Controlled active item name.
defaultItem?string
Initially active item name. Defaults to the first enabled item.
onItemChange?(item: string) → void
Called when navigation requests a different item.
- Parameters
item– string- Returns
void
items?readonly QuestionnaireItemDefinition[]
Optional item data for server rendering and item order.
- Type
readonly QuestionnaireItemDefinition[]
noValidate?boolean
Disables native constraint UI while preserving questionnaire validation.
- Default
true
onReset?FormEventHandler<HTMLFormElement>
Native reset handler. Prevent default to stop questionnaire reset.
- Type
FormEventHandler<HTMLFormElement>
onSubmit?FormEventHandler<HTMLFormElement>
Native form submission handler after every item validates.
- Type
FormEventHandler<HTMLFormElement>
shortcuts?"letters" | "numbers"
Assigns scoped answer shortcuts in definition or rendered DOM order.
- Type
"letters" | "numbers"
className?string
Merged with the part's classes.
data-currentstring
One-based active item position.
data-first?string
Present on the first item.
data-last?string
Present on the last item.
data-shortcuts?"letters" | "numbers"
"letters" or "numbers", when enabled.
- Type
"letters" | "numbers"
data-totalstring
Number of enabled items.
QuestionnaireActions
<div> data-slot="questionnaire-actions"
className?string
Merged with the part's classes.
QuestionnaireChoice
<label> data-slot="questionnaire-choice"
valuestring
Native answer value.
checked?boolean
Controlled checked state.
defaultChecked?boolean
Initial checked state.
- Default
false
disabled?boolean
Disables the native answer control.
- Default
false
onChange?React.ChangeEventHandler<HTMLInputElement>
Native input change handler.
- Type
React.ChangeEventHandler<HTMLInputElement>
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with choice state. Its function's state has checked, disabled, invalid, shortcut and type.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<label>
className?string
Merged with the part's classes.
data-checked?string
Present when selected.
data-disabled?string
Present when disabled.
data-invalid?string
Present when its item is invalid.
data-shortcutstring
Assigned letter or number.
data-type"radio" | "checkbox"
"radio" or "checkbox".
- Type
"radio" | "checkbox"
data-unchecked?string
Present when not selected.
QuestionnaireChoiceDescription
<span> data-slot="questionnaire-choice-description"
className?string
Merged with the part's classes.
QuestionnaireChoices
<div> data-slot="questionnaire-choices"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target. Its function's state has shortcuts.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<div>
className?string
Merged with the part's classes.
data-shortcuts?"letters" | "numbers"
"letters" or "numbers", when enabled.
- Type
"letters" | "numbers"
QuestionnaireDescription
<p> data-slot="questionnaire-description"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<p>
className?string
Merged with the part's classes.
QuestionnaireError
<p> data-slot="questionnaire-error"
children?React.ReactNode
Custom validation message. Defaults to the contextual message.
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with invalid state. Its function's state has invalid.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<p>
className?string
Merged with the part's classes.
data-invalid?string
Present while the error is active.
QuestionnaireInput
<div> data-slot="questionnaire-input"
defaultValue?string | number | readonly string[]
Initial native input value.
- Type
string | number | readonly string[]
disabled?boolean
Disables the input.
- Default
false
onChange?ChangeEventHandler<HTMLInputElement>
Native input change handler.
- Type
ChangeEventHandler<HTMLInputElement>
type?"date" | "datetime-local" | "email" | "month" | "number" | "password" | "search" | "tel" | "text" | "time" | "url" | "week"
A text-entry type such as "email", "number", or "date".
- Type
"date" | "datetime-local" | "email" | "month" | "number" | "password" | "search" | "tel" | "text" | "time" | "url" | "week"- Default
"text"
value?string | number | readonly string[]
Controlled native input value.
- Type
string | number | readonly string[]
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with input state. Its function's state has disabled, filled and invalid.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<input>
className?string
Merged with the part's classes.
data-disabled?string
Present when disabled.
data-empty?string
Present when the input has no answer text.
data-filled?string
Present when the input has non-empty text.
data-invalid?string
Present when its item is invalid.
QuestionnaireItem
<fieldset> data-slot="questionnaire-item"
namestring
Unique item identifier and native form field name.
disabled?boolean
Omits the item from progress and navigation.
- Default
false
invalid?boolean
Marks the item invalid from an external validator.
- Default
false
multiple?boolean
Renders fixed choices as checkboxes.
- Default
false
onStatusChange?(status: QuestionnaireItemStatus) → void
Observes "unanswered", "answered", and "skipped" changes.
- Type
(status: QuestionnaireItemStatus) → void- Parameters
status– QuestionnaireItemStatus- Returns
void
required?boolean
Requires an answer and prevents Skip.
- Default
false
className?string
Merged with the part's classes.
data-active?string
Present when active.
data-disabled?string
Present when disabled.
data-invalid?string
Present after failed validation.
data-multiple?string
Present when multiple.
data-required?string
Present when required.
data-status"unanswered" | "answered" | "skipped"
"unanswered", "answered" or "skipped".
- Type
"unanswered" | "answered" | "skipped"
QuestionnaireNext
<button> data-slot="questionnaire-next"
size?"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"
The size.
- Type
"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"- Default
"md"
variant?"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"
The visual style.
- Type
"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"- Default
"default"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with navigation state. Its function's state has disabled, shortcut, status and visible.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Merged with the part's classes.
data-disabled?string
Present when disabled.
data-hidden?string
Present when the action does not apply.
data-shortcut?"Enter"
"Enter", on an enabled, visible Next or Submit.
data-size?string
Mirrors size.
data-status"unanswered" | "answered" | "skipped"
Active item: "unanswered", "answered", or "skipped".
- Type
"unanswered" | "answered" | "skipped"
data-variant?string
Mirrors variant.
data-visible?string
Present when the action applies to the active item.
QuestionnairePrevious
<button> data-slot="questionnaire-previous"
size?"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"
The size.
- Type
"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"- Default
"md"
variant?"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"
Button's default: default.
- Type
"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"- Default
"outline"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with navigation state. Its function's state has disabled, shortcut, status and visible.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Merged with the part's classes.
data-disabled?string
Present when disabled.
data-hidden?string
Present when the action does not apply.
data-shortcut?"Enter"
"Enter", on an enabled, visible Next or Submit.
data-size?string
Mirrors size.
data-status"unanswered" | "answered" | "skipped"
Active item: "unanswered", "answered", or "skipped".
- Type
"unanswered" | "answered" | "skipped"
data-variant?string
Mirrors variant.
data-visible?string
Present when the action applies to the active item.
QuestionnaireProgress
<div> data-slot="questionnaire-progress"
aria-label?string
Accessible progressbar name.
- Default
Questionnaire progress
children?React.ReactNode
Custom progress content.
- Default
Question {current} of {total}
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with progress state. Its function's state has current, first, last and total.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<div>
className?string
Merged with the part's classes.
data-currentstring
One-based active item position.
data-first?string
Present on the first item.
data-last?string
Present on the last item.
data-totalstring
Number of enabled items.
QuestionnaireSkip
<button> data-slot="questionnaire-skip"
size?"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"
The size.
- Type
"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"- Default
"md"
variant?"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"
Button's default: default.
- Type
"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"- Default
"outline"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with navigation state. Its function's state has disabled, shortcut, status and visible.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Merged with the part's classes.
data-disabled?string
Present when disabled.
data-hidden?string
Present when the action does not apply.
data-shortcut?"Enter"
"Enter", on an enabled, visible Next or Submit.
data-size?string
Mirrors size.
data-status"unanswered" | "answered" | "skipped"
Active item: "unanswered", "answered", or "skipped".
- Type
"unanswered" | "answered" | "skipped"
data-variant?string
Mirrors variant.
data-visible?string
Present when the action applies to the active item.
QuestionnaireSubmit
<button> data-slot="questionnaire-submit"
size?"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"
The size.
- Type
"default" | "icon" | "icon-lg" | "icon-md" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "md" | "sm" | "xl" | "xs"- Default
"md"
variant?"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"
The visual style.
- Type
"default" | "destructive" | "ghost" | "link" | "outline" | "secondary"- Default
"default"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target with navigation state. Its function's state has disabled, shortcut, status and visible.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Merged with the part's classes.
data-disabled?string
Present when disabled.
data-hidden?string
Present when the action does not apply.
data-shortcut?"Enter"
"Enter", on an enabled, visible Next or Submit.
data-size?string
Mirrors size.
data-status"unanswered" | "answered" | "skipped"
Active item: "unanswered", "answered", or "skipped".
- Type
"unanswered" | "answered" | "skipped"
data-variant?string
Mirrors variant.
data-visible?string
Present when the action applies to the active item.
QuestionnaireTitle
<legend> data-slot="questionnaire-title"
render?ReactElement | ((props: object, state: object) => ReactElement)
Custom render target. Pair its id with the item’s aria-labelledby.
- Type
ReactElement | ((props: object, state: object) => ReactElement)- Default
<legend>
className?string
Merged with the part's classes.











