- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
Questionnaire is an unstyled form primitive for presenting one question at a
time. It manages answers, progress, validation, and navigation.
It works well for agent clarification prompts, onboarding, surveys, intake forms, and configuration.
The unstyled package gives you full control over markup and styles. For the styled version and themed examples, see Questionnaire.
Installation#
pnpm add @shadcn/react
Import#
import { Questionnaire } from "@shadcn/react/questionnaire"Questionnaire exports its parts from one namespace. Each part accepts the
native props for its default element.
Anatomy#
<Questionnaire.Root>
<Questionnaire.Progress />
<Questionnaire.Item name="question">
<Questionnaire.Title />
<Questionnaire.Description />
<Questionnaire.Choices>
<Questionnaire.Choice>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel />
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
<Questionnaire.Input />
</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>
<Questionnaire.Previous />
<Questionnaire.Skip />
<Questionnaire.Next />
<Questionnaire.Submit />
</Questionnaire.Root>Root renders a form. Each Item is a fieldset, with Title as its legend.
ChoiceInput renders a native radio or checkbox.
Styled version#
The styled registry component uses flat component names:
| Styled component | Unstyled part |
|---|---|
Questionnaire | Questionnaire.Root |
QuestionnaireProgress | Questionnaire.Progress |
QuestionnaireItem | Questionnaire.Item |
QuestionnaireTitle | Questionnaire.Title |
QuestionnaireDescription | Questionnaire.Description |
QuestionnaireChoices | Questionnaire.Choices |
QuestionnaireChoice | Questionnaire.Choice with ChoiceInput, ChoiceLabel, and ChoiceShortcut |
QuestionnaireInput | Questionnaire.Input |
QuestionnaireError | Questionnaire.Error |
QuestionnaireActions | None. Layout only; use your own container. |
QuestionnairePrevious, QuestionnaireSkip, QuestionnaireNext, QuestionnaireSubmit | Questionnaire.Previous, Questionnaire.Skip, Questionnaire.Next, Questionnaire.Submit |
The styled QuestionnaireChoice composes the input, label, shortcut, and visual
indicator for you. With the unstyled package, compose those parts yourself.
Basic usage#
Each Item is one step. Its name identifies the step and becomes the form
field name for its answers. Choice.value is the submitted answer.
const items = [
{
name: "prototype",
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"use client"
import * as React from "react"
import { Questionnaire } from "@shadcn/react/questionnaire"
export function ProjectQuestionnaire() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
console.log({
prototype: formData.get("prototype"),
detail: formData.get("detail"),
})
}
return (
<Questionnaire.Root items={items} onSubmit={handleSubmit}>
<Questionnaire.Progress />
{items.map((question) => (
<Questionnaire.Item
key={question.name}
name={question.name}
required={question.required}
>
<Questionnaire.Title>{question.prompt}</Questionnaire.Title>
<Questionnaire.Description>
{question.description}
</Questionnaire.Description>
<Questionnaire.Choices>
{question.choices.map((choice) => (
<Questionnaire.Choice key={choice.value} value={choice.value}>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>
<span>{choice.label}</span>
{"description" in choice ? (
<span>{choice.description}</span>
) : null}
</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
))}
{"input" in question ? (
<Questionnaire.Input
aria-label={question.input.label}
placeholder={question.input.placeholder}
/>
) : null}
</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>
))}
<Questionnaire.Previous />
<Questionnaire.Skip />
<Questionnaire.Next />
<Questionnaire.Submit />
</Questionnaire.Root>
)
}Pass the same items collection to Root that you render as Item and
Choice parts. This makes item order, progress, action visibility, and answer
shortcuts available in the server-rendered HTML.
Multiple selection#
multiple turns an item's fixed choices into native checkboxes. Read the answers with
FormData.getAll(). Keep multiple in your application data and pass it to
the rendered Item.
const items = [
{
name: "signals",
required: true,
multiple: true,
prompt: "What should every update include?",
description: "Select all that apply.",
choices: [
{ value: "progress", label: "Progress" },
{ value: "decisions", label: "Decisions" },
{ value: "risks", label: "Risks" },
],
},
] as constitems.map((question) => (
<Questionnaire.Item
key={question.name}
name={question.name}
multiple={question.multiple}
required={question.required}
>
<Questionnaire.Title>{question.prompt}</Questionnaire.Title>
<Questionnaire.Description>
{question.description}
</Questionnaire.Description>
<Questionnaire.Choices>
{question.choices.map((choice) => (
<Questionnaire.Choice key={choice.value} value={choice.value}>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>{choice.label}</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
))}
</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>
))const signals = new FormData(form).getAll("signals").map(String)Freeform answers#
Input adds a freeform answer and renders a native text input.
const items = [
{
name: "prototype",
required: true,
prompt: "What should we prototype next?",
choices: [
{ value: "delegation", label: "Delegation" },
{ value: "questions", label: "Question prompts" },
],
input: {
label: "Another prototype direction",
placeholder: "Type another directionโฆ",
},
},
] as constitems.map((question) => (
<Questionnaire.Item
key={question.name}
name={question.name}
required={question.required}
>
<Questionnaire.Title>{question.prompt}</Questionnaire.Title>
<Questionnaire.Choices>
{question.choices.map((choice) => (
<Questionnaire.Choice key={choice.value} value={choice.value}>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>{choice.label}</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
))}
<Questionnaire.Input
aria-label={question.input.label}
placeholder={question.input.placeholder}
/>
</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>
))Explicit skip#
Skip records that an optional item was intentionally left unanswered. Use
onStatusChange when your application needs to distinguish a skipped answer
from a missing one.
"use client"
import * as React from "react"
import {
Questionnaire,
type QuestionnaireItemStatus,
} from "@shadcn/react/questionnaire"
export function PlanningQuestionnaire() {
const [timingStatus, setTimingStatus] =
React.useState<QuestionnaireItemStatus>("unanswered")
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
console.log({
timing:
timingStatus === "skipped"
? { status: "skipped" }
: {
status: "answered",
value: formData.get("timing"),
},
})
}
return (
<Questionnaire.Root defaultItem="timing" onSubmit={handleSubmit}>
<Questionnaire.Item name="timing" onStatusChange={setTimingStatus}>
<Questionnaire.Title>
When should this be revisited?
</Questionnaire.Title>
<Questionnaire.Description>
Skip this if timing has not been decided.
</Questionnaire.Description>
<Questionnaire.Choices>
<Questionnaire.Choice value="week">
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>This week</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
<Questionnaire.Choice value="cycle">
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>Next cycle</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
</Questionnaire.Choices>
</Questionnaire.Item>
<Questionnaire.Skip />
<Questionnaire.Submit />
</Questionnaire.Root>
)
}Answer shortcuts#
Use shortcuts="letters" or shortcuts="numbers" to assign a key to each
enabled fixed choice, following the items order when it is provided. Compose
ChoiceShortcut wherever its hint should appear.
<Questionnaire.Root shortcuts="letters">
<Questionnaire.Item name="review" required>
<Questionnaire.Title>What should the agent review?</Questionnaire.Title>
<Questionnaire.Choices>
<Questionnaire.Choice value="api">
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>Public API</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
<Questionnaire.Choice value="tests">
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>Test coverage</Questionnaire.ChoiceLabel>
<Questionnaire.ChoiceShortcut />
</Questionnaire.Choice>
</Questionnaire.Choices>
</Questionnaire.Item>
</Questionnaire.Root>"letters" assigns A through Z; "numbers" assigns 1 through 9.
Disabled choices are skipped. Selecting an answer by shortcut does not advance
to the next item.
Validation#
Questionnaire validates the active item before moving forward and validates all enabled items when the form submits.
- A required item is valid after it has an answer.
- An optional item is valid after it has an answer or is explicitly skipped.
- Disabled items and answers are ignored.
required does not add visible โRequiredโ text. Say it in the Title or
Description.
When validation fails, Questionnaire keeps or opens the invalid item and
focuses an answer. Add Error to show a message.
<Questionnaire.Item name="scope" required>
<Questionnaire.Title>
What should the project include? (Required)
</Questionnaire.Title>
<Questionnaire.Choices>{/* choices */}</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>Error remains hidden until the item is invalid. Pass children to replace its
default message.
<Questionnaire.Error>Please choose a project scope.</Questionnaire.Error>Validation still works without Error. When rendered, the message is announced
to screen readers.
For Zod or another external validator, set Item.invalid, render the message in
Error, and move Root.item to the first invalid item.
<Questionnaire.Root item={item} onItemChange={setItem} onSubmit={handleSubmit}>
<Questionnaire.Item invalid={Boolean(errors.detail)} name="detail" required>
<Questionnaire.Title>How much detail?</Questionnaire.Title>
<Questionnaire.Choices>{/* choices */}</Questionnaire.Choices>
<Questionnaire.Error>{errors.detail}</Questionnaire.Error>
</Questionnaire.Item>
</Questionnaire.Root>Controlled navigation#
Pass item and onItemChange to control the active item.
Current checkpoint: Change scope
const [item, setItem] = React.useState("scope")
<Questionnaire.Root item={item} onItemChange={setItem} onSubmit={handleSubmit}>
<Questionnaire.Item name="scope" required>
{/* question and answers */}
</Questionnaire.Item>
<Questionnaire.Item name="verification" required>
{/* question and answers */}
</Questionnaire.Item>
<Questionnaire.Previous />
<Questionnaire.Next>Next</Questionnaire.Next>
<Questionnaire.Submit>Submit</Questionnaire.Submit>
</Questionnaire.Root>Resume with defaults#
Restore a saved draft with defaultItem, defaultChecked, and defaultValue.
<Questionnaire.Root defaultItem="verification">
<Questionnaire.Item name="scope" required>
<Questionnaire.Title>Which files are in scope?</Questionnaire.Title>
<Questionnaire.Choices>
<Questionnaire.Choice value="component" defaultChecked>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>Component only</Questionnaire.ChoiceLabel>
</Questionnaire.Choice>
</Questionnaire.Choices>
</Questionnaire.Item>
<Questionnaire.Item name="verification" required>
<Questionnaire.Title>Any extra instructions?</Questionnaire.Title>
<Questionnaire.Input
aria-label="Extra instructions"
defaultValue="Run the package tests."
/>
</Questionnaire.Item>
<button type="reset">Reset draft</button>
</Questionnaire.Root>Conditional items#
Set disabled to remove an item from the current flow. Disabled items are
excluded from progress, navigation, validation, and submission.
const [runtime, setRuntime] = React.useState("local")
<Questionnaire.Root>
<Questionnaire.Item name="runtime" required>
<Questionnaire.Title>Where should the agent run?</Questionnaire.Title>
<Questionnaire.Choices>
<Questionnaire.Choice
value="local"
checked={runtime === "local"}
onChange={() => setRuntime("local")}
>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>Locally</Questionnaire.ChoiceLabel>
</Questionnaire.Choice>
<Questionnaire.Choice
value="remote"
checked={runtime === "remote"}
onChange={() => setRuntime("remote")}
>
<Questionnaire.ChoiceInput />
<Questionnaire.ChoiceLabel>
Remote environment
</Questionnaire.ChoiceLabel>
</Questionnaire.Choice>
</Questionnaire.Choices>
</Questionnaire.Item>
<Questionnaire.Item name="region" disabled={runtime !== "remote"} required>
{/* remote-only question */}
</Questionnaire.Item>
</Questionnaire.Root>Navigation state#
Navigation actions stay enabled by default so activating Next or Submit can show a validation error. Use the render state when you want to disable an action yourself.
<Questionnaire.Next
render={(props, state) => (
<button {...props} disabled={state.status === "unanswered"} />
)}
>
Next
</Questionnaire.Next>The render state contains visible, disabled, shortcut, and the active
item's status. To change only the appearance, target data-status instead:
<Questionnaire.Next data-navigation-action>Next</Questionnaire.Next>[data-navigation-action][data-status="unanswered"] {
opacity: 0.5;
}Custom progress#
Progress renders Question {current} of {total} by default. Use render to
change its element or format.
<Questionnaire.Progress
aria-label="Setup progress"
render={(props, state) => (
<output {...props}>
Checkpoint {state.current} of {state.total}
</output>
)}
/>The render state contains current, total, first, and last. Pass a
localized aria-label when needed.
Animated items#
Animate the active item while keeping progress and navigation stationary.
const itemClassName =
"data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none"
<Questionnaire.Item
className={itemClassName}
name="task"
required
>
{/* ... */}
</Questionnaire.Item>Inactive items hide immediately, so animate the entry only.
Card composition#
Place the root around a card and render Title and Description into the
card header. Give the title an id and connect it with the item's
aria-labelledby since it replaces the default legend.
const titleId = React.useId()
<Questionnaire.Root onSubmit={handleSubmit}>
<Card>
<Questionnaire.Item aria-labelledby={titleId} name="task" required>
<CardHeader>
<Questionnaire.Title id={titleId} render={<CardTitle />}>
What should the agent work on?
</Questionnaire.Title>
<Questionnaire.Description render={<CardDescription />}>
Choose the task that should be handled next.
</Questionnaire.Description>
<CardAction>
<Questionnaire.Progress />
</CardAction>
</CardHeader>
<CardContent>
<Questionnaire.Choices>{/* choices */}</Questionnaire.Choices>
<Questionnaire.Error />
</CardContent>
</Questionnaire.Item>
<CardFooter>
<Questionnaire.Next>Next</Questionnaire.Next>
<Questionnaire.Submit>Submit</Questionnaire.Submit>
</CardFooter>
</Card>
</Questionnaire.Root>Dialog composition#
Keep dialog dismissal separate from Skip: closing cancels the flow, while
skipping records an intentional unanswered item.
const titleId = React.useId()
<Dialog>
<DialogTrigger>Open clarification</DialogTrigger>
<DialogContent>
<Questionnaire.Root onSubmit={handleSubmit}>
<Questionnaire.Item aria-labelledby={titleId} name="scope" required>
<DialogHeader>
<Questionnaire.Progress />
<Questionnaire.Title id={titleId} render={<DialogTitle />}>
Which files are in scope?
</Questionnaire.Title>
<Questionnaire.Description render={<DialogDescription />}>
Choose how broadly the agent can update the workspace.
</Questionnaire.Description>
</DialogHeader>
<Questionnaire.Choices>{/* choices */}</Questionnaire.Choices>
<Questionnaire.Error />
</Questionnaire.Item>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<Questionnaire.Next>Next</Questionnaire.Next>
<Questionnaire.Submit>Send answer</Questionnaire.Submit>
</DialogFooter>
</Questionnaire.Root>
</DialogContent>
</Dialog>Custom render targets#
Use render to replace a part's default element. Pass an element, or use a
function when you need its state.
<Questionnaire.Next render={<Button />}>Next</Questionnaire.Next>Root and Item always render a form and fieldset. If Title no longer
renders a legend, give it an id and pass that ID to Item with
aria-labelledby.
The primitive does not emit data-slot. Styled wrappers own slots and visual
indicators.
Native forms#
Root always renders a native form and supports onSubmit, onReset, action,
and the other form props. Do not nest a Questionnaire inside another form.
Answers serialize through native controls:
FormData.get(itemName)reads a single answer.FormData.getAll(itemName)reads multiple answers.- Skipped items are absent from
FormData. - Use
Item.onStatusChangeto distinguish a skip from a missing answer.
Root sets noValidate by default so validation uses Questionnaire.Error
instead of the browser's constraint validation UI.
form.reset() restores the initial item, default answers, skip state, and
validation state.
Keyboard navigation#
Questionnaire builds on native radio, checkbox, input, and button behavior.
| Key | Behavior |
|---|---|
Tab | Moves focus between answer controls and visible actions. |
Shift + Tab | Moves focus to the previous control or action. |
ArrowUp | Moves to the previous answer from the item, a fixed answer, or an empty text-like freeform input. Native radios also select it. |
ArrowDown | Moves to the next answer from the item, a fixed answer, or an empty text-like freeform input. Native radios also select it. |
ArrowLeft | Moves to the previous item when focus is outside a radio or text entry control. |
ArrowRight | Moves to the next item when the active item is answered or skipped and focus is outside a radio or text entry control. |
Space | Selects a radio, toggles a checkbox, or activates a focused action. |
Enter | Continues from a selected choice or selected, filled input; activates a focused action. |
Command/Ctrl + Enter | Validates and continues from anywhere inside the questionnaire, or submits the final item. |
Shortcuts and arrow navigation pause while you type in a text field.
Preventing the root onKeyDown event turns off questionnaire key handling.
Navigation actions stay enabled by default so an attempted action can reveal validation feedback. See Navigation state to disable or restyle an unanswered action.
Accessibility#
Itemrenders afieldset.Titlerenders the fieldsetlegendby default. If it uses a custom render target, connect itsidto the item witharia-labelledby.Descriptionand the activeErrorare connected witharia-describedby.- Invalid items and answer controls expose
aria-invalid. Progressrenders a named progressbar with current, minimum, maximum, and text values.- Fixed choices use native radios and checkboxes.
- Assigned shortcut keys and available navigation are exposed with
aria-keyshortcuts. - Inactive items and actions are hidden and inert.
- Navigation focuses the newly active fieldset.
- Validation focuses the first available answer control.
- Disabled items are omitted from progress and navigation.
Always give Input an accessible name. Use an explicit id with a visible
label:
<Label htmlFor="other-answer">Other answer</Label>
<Questionnaire.Input
id="other-answer"
placeholder="Type another answerโฆ"
/>When the design has no visible label, use aria-label or aria-labelledby:
<Questionnaire.Input
aria-label="Other answer"
placeholder="Type another answerโฆ"
/>A placeholder is not a label.
Data attributes#
Use these attributes for styling. Boolean attributes are present when true and absent when false.
Root and Progress#
| Data attribute | Value |
|---|---|
data-current | One-based active item position. |
data-total | Number of enabled items. |
data-first | Present on the first item. |
data-last | Present on the last item. |
data-shortcuts | "letters" | "numbers" on Root when enabled. |
Item#
| Data attribute | Value |
|---|---|
data-active | Present when active. |
data-status | "unanswered" | "answered" | "skipped" |
data-required | Present when required. |
data-multiple | Present when multiple. |
data-disabled | Present when disabled. |
data-invalid | Present after failed validation. |
Choice#
| Data attribute | Value |
|---|---|
data-type | "radio" | "checkbox" |
data-checked | Present when selected. |
data-unchecked | Present when not selected. |
data-disabled | Present when disabled. |
data-invalid | Present when its item is invalid. |
data-shortcut | Assigned letter or number. |
Choices#
| Data attribute | Value |
|---|---|
data-shortcuts | "letters" | "numbers" when enabled. |
Input#
| Data attribute | Value |
|---|---|
data-filled | Present when the input has non-empty text. |
data-empty | Present when the input has no answer text. |
data-disabled | Present when disabled. |
data-invalid | Present when its item is invalid. |
Error#
| Data attribute | Value |
|---|---|
data-invalid | Present while the error is active. |
Previous, Skip, Next, and Submit#
| Data attribute | Value |
|---|---|
data-visible | Present when the action applies to the active item. |
data-hidden | Present when the action does not apply. |
data-disabled | Present when disabled. |
data-shortcut | "Enter" on an enabled, visible Next or Submit. |
data-status | Active item: "unanswered", "answered", or "skipped". |
Title and Description have no state attributes. The headless parts do not
emit data-slot.
API Reference#
Questionnaire.Root#
The form and coordination root.
| Prop | Type | Default | Description |
|---|---|---|---|
item | string | - | Controlled active item name. |
defaultItem | string | first enabled item | Initially active item name. |
items | readonly QuestionnaireItemDefinition[] | - | Optional item data for server rendering and item order. |
onItemChange | (item: string) => void | - | Called when navigation requests a different item. |
shortcuts | "letters" | "numbers" | - | Assigns scoped answer shortcuts in definition or rendered DOM order. |
noValidate | boolean | true | Disables native constraint UI while preserving questionnaire validation. |
onSubmit | FormEventHandler<HTMLFormElement> | - | Native form submission handler after every item validates. |
onReset | FormEventHandler<HTMLFormElement> | - | Native reset handler. Prevent default to stop questionnaire reset. |
Root exposes current, total, first, last, and shortcuts through data
attributes.
The optional collection types are exported from
@shadcn/react/questionnaire:
type QuestionnaireChoiceDefinition = {
value: string
disabled?: boolean
}
type QuestionnaireItemDefinition = {
name: string
required?: boolean
disabled?: boolean
choices?: readonly QuestionnaireChoiceDefinition[]
}Questionnaire.Progress#
Position within the enabled item collection.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | Question {current} of {total} | Custom progress content. |
aria-label | string | Questionnaire progress | Accessible progressbar name. |
render | ReactElement | (props, state) => ReactElement | <div> | Custom render target with progress state. |
The render state contains current, total, first, and last.
Questionnaire.Item#
One questionnaire step.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique item identifier and native form field name. |
invalid | boolean | false | Marks the item invalid from an external validator. |
required | boolean | false | Requires an answer and prevents Skip. |
multiple | boolean | false | Renders fixed choices as checkboxes. |
disabled | boolean | false | Omits the item from progress and navigation. |
onStatusChange | (status: QuestionnaireItemStatus) => void | - | Observes "unanswered", "answered", and "skipped" changes. |
Item exposes active, disabled, invalid, multiple, required, and
status through data attributes.
Item names must be unique within a Root. The name belongs to the answer controls, not the fieldset.
Questionnaire.Title#
The item title. Renders a semantic legend by default.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | render function | <legend> | Custom render target. Pair its id with the itemโs aria-labelledby. |
Questionnaire.Description#
Supporting text connected to the item with aria-describedby.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | render function | <p> | Custom render target. |
Questionnaire.Choices#
A layout container for fixed and freeform answers.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | render function | <div> | Custom render target. |
The render state contains shortcuts.
Questionnaire.Choice#
A fixed-answer container. Compose one ChoiceInput, one ChoiceLabel, and an
optional ChoiceShortcut inside it. The default <label> keeps the whole row
associated with its native control.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Native answer value. |
checked | boolean | - | Controlled checked state. |
defaultChecked | boolean | false | Initial checked state. |
disabled | boolean | false | Disables the native answer control. |
onChange | ChangeEventHandler<HTMLInputElement> | - | Native input change handler. |
render | ReactElement | (props, state) => ReactElement | <label> | Custom render target with choice state. |
The render state contains checked, disabled, invalid, shortcut, and
type.
Questionnaire.ChoiceInput#
The native radio or checkbox for its containing Choice. Questionnaire supplies
the props needed for selection, form submission, validation, and keyboard
interaction. It also accepts className, id, ref, render, and other
non-conflicting native input props.
ChoiceInput must be used inside Choice. Its render state matches Choice.
Questionnaire.ChoiceLabel#
The visible label content for a fixed choice. It renders a <span> inside the
Choice label and accepts native span props and render.
Questionnaire.ChoiceShortcut#
The visible shortcut for a fixed choice. It renders a <span> containing the
assigned letter or number and remains hidden when the choice has no shortcut.
It accepts native span props and render; its render state contains shortcut.
Questionnaire.Input#
A freeform answer. Its native name is managed by the containing item.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | number | readonly string[] | - | Controlled native input value. |
defaultValue | string | number | readonly string[] | - | Initial native input value. |
disabled | boolean | false | Disables the input. |
onChange | ChangeEventHandler<HTMLInputElement> | - | Native input change handler. |
type | QuestionnaireInputType | "text" | A text-entry type such as "email", "number", or "date". |
render | ReactElement | (props, state) => ReactElement | <input> | Custom render target with input state. |
The render state contains disabled, filled, and invalid.
Questionnaire.Error#
The validation message. It is hidden until its item fails validation.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | contextual message | Custom validation message. |
render | ReactElement | (props, state) => ReactElement | <p> | Custom render target with invalid state. |
Navigation actions#
Previous, Skip, Next, and Submit render native buttons and stay mounted
while their visibility changes.
| Part | Default type | Visible when | Disabled when |
|---|---|---|---|
Previous | "button" | Not on the first item. | Consumer sets disabled. |
Skip | "button" | Active item is optional. | Consumer sets disabled. |
Next | "button" | Not on the last item. | Consumer sets disabled. |
Submit | "submit" | On the last item. | Consumer sets disabled. |
Each accepts native button props and:
| Prop | Type | Description |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | Custom render target with navigation state. |
The render state contains visible, disabled, shortcut, and the active
item's status.
On This Page
InstallationImportAnatomyStyled versionBasic usageMultiple selectionFreeform answersExplicit skipAnswer shortcutsValidationControlled navigationResume with defaultsConditional itemsNavigation stateCustom progressAnimated itemsCard compositionDialog compositionCustom render targetsNative formsKeyboard navigationAccessibilityData attributesRoot and ProgressItemChoiceChoicesInputErrorPrevious, Skip, Next, and SubmitAPI ReferenceQuestionnaire.RootQuestionnaire.ProgressQuestionnaire.ItemQuestionnaire.TitleQuestionnaire.DescriptionQuestionnaire.ChoicesQuestionnaire.ChoiceQuestionnaire.ChoiceInputQuestionnaire.ChoiceLabelQuestionnaire.ChoiceShortcutQuestionnaire.InputQuestionnaire.ErrorNavigation actions