The styled QuestionnaireChoice composes the input, label, shortcut, and visual
indicator for you. With the unstyled package, compose those parts yourself.
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 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 const
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> )}
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.
"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.
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.
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>
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.
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.
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.
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.