- Accordion
- Alert
- Alert Dialog
- Attachment
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Checkbox
- Collapsible
- Command
- Context Menu
- Data Table
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const questionnaireItems = [
{
choices: [
{
description: "Show what the agent ran and what came back.",
label: "Tool call timeline",
value: "tool-calls",
},
{
description: "Ask before sensitive or destructive actions.",
label: "Approval checkpoints",
value: "approvals",
},
{
description: "Make delegated work and results easier to follow.",
label: "Sub-agent handoffs",
value: "handoffs",
},
],
description: "Choose a direction or describe another task.",
input: {
label: "Another agent feature",
placeholder: "Describe another feature…",
},
name: "direction",
required: true,
title: "What should the agent build next?",
},
{
choices: [
{ label: "Progress", value: "progress" },
{ label: "Decisions", value: "decisions" },
{ label: "Risks", value: "risks" },
{ label: "Next step", value: "next-step" },
],
description: "Select all that apply, or skip this question.",
multiple: true,
name: "signals",
required: false,
title: "What should every progress update include?",
},
{
choices: [
{ label: "Start now", value: "now" },
{ label: "Next development cycle", value: "next-cycle" },
{ label: "Add it to the backlog", value: "backlog" },
],
description: "Choose when the agent should begin the work.",
name: "timing",
required: true,
title: "When should work begin?",
},
] as const
export function QuestionnaireDemo() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const answers = {
direction: formData.get("direction"),
signals: formData.getAll("signals"),
timing: formData.get("timing"),
}
toast("Agent plan saved", {
description: `Direction: ${answers.direction ?? "None"} · Progress signals: ${answers.signals.join(", ") || "None"} · Timing: ${answers.timing ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="direction"
items={questionnaireItems}
shortcuts="letters"
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
{questionnaireItems.map((question) => (
<QuestionnaireItem
key={question.name}
multiple={"multiple" in question && question.multiple}
name={question.name}
required={question.required}
>
<QuestionnaireTitle>{question.title}</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>Next</QuestionnaireNext>
<QuestionnaireSubmit>Save plan</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Questionnaire asks one question at a time: fixed choices as Field-style choice cards with the Checkbox and Radio skins, an optional freeform input, progress and navigation. The behavior comes from @shadcn/react.
Installation#
pnpm dlx shadcn@latest add https://ui.ikramhasan.com/r/questionnaire.json
Usage#
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/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" },
{ value: "questions", label: "Question prompts" },
],
},
] as constDefine the items once: pass them to Questionnaire for server-rendered progress, actions and shortcuts, then map them 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}>
{choice.label}
</QuestionnaireChoice>
))}
</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
│ │ │ └── QuestionnaireChoiceDescription
│ │ └── QuestionnaireInput
│ └── QuestionnaireError
└── QuestionnaireActions
├── QuestionnairePrevious
├── QuestionnaireSkip
├── QuestionnaireNext
└── QuestionnaireSubmitQuestionnaire owns the ordered items, the active item, answers, validation, progress and navigation. The page, card, dialog or drawer around it owns closing, persistence, transport and branching.
Examples#
Multiple Selection#
Use multiple on an item that accepts more than one answer. Its choices take the Checkbox skin.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{
choices: [
{ value: "source" },
{ value: "tests" },
{ value: "docs" },
{ value: "history" },
],
name: "context",
required: true,
},
] as const
export function QuestionnaireMultiple() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const context = new FormData(event.currentTarget).getAll("context")
toast("Context selected", {
description: `Context: ${context.join(", ") || "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
items={items}
shortcuts="letters"
onSubmit={handleSubmit}
>
<QuestionnaireItem name="context" multiple required>
<QuestionnaireTitle>
What context should the agent inspect?
</QuestionnaireTitle>
<QuestionnaireDescription>
Select every source that may affect the implementation.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="source">
Relevant source files
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Existing tests
</QuestionnaireChoice>
<QuestionnaireChoice value="docs">
Architecture documentation
</QuestionnaireChoice>
<QuestionnaireChoice value="history">
Recent commit history
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>Share context</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Freeform Answer#
Add a QuestionnaireInput next to the fixed choices for another answer.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{
choices: [
{ value: "incremental" },
{ value: "module" },
{ value: "rewrite" },
],
name: "approach",
required: true,
},
] as const
export function QuestionnaireFreeform() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const approach = new FormData(event.currentTarget).get("approach")
toast("Approach selected", {
description: `Approach: ${approach ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
items={items}
shortcuts="letters"
onSubmit={handleSubmit}
>
<QuestionnaireItem name="approach" required>
<QuestionnaireTitle>
How should the agent approach this refactor?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose a strategy or write a more specific instruction.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="incremental">
Make the smallest safe change
</QuestionnaireChoice>
<QuestionnaireChoice value="module">
Refactor one module at a time
</QuestionnaireChoice>
<QuestionnaireChoice value="rewrite">
Replace the implementation completely
</QuestionnaireChoice>
<QuestionnaireInput
aria-label="Another refactoring approach"
placeholder="Describe another approach…"
/>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>Use this approach</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Explicit Skip#
Add QuestionnaireSkip when an optional item may be left unanswered.
"use client"
import * as React from "react"
import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "task", required: true },
{ name: "constraints" },
{ name: "review", required: true },
] as const
export function QuestionnaireSkipExample() {
const [constraintStatus, setConstraintStatus] =
React.useState<QuestionnaireItemStatus>("unanswered")
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const answers = {
task: formData.get("task"),
constraints: formData.get("constraints"),
constraintStatus,
review: formData.get("review"),
}
toast("Agent brief submitted", {
description: `Task: ${answers.task ?? "None"} · Constraints: ${
answers.constraintStatus === "skipped"
? "Skipped"
: (answers.constraints ?? "None")
} · Review: ${answers.review ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="task"
items={items}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem name="task" required>
<QuestionnaireTitle>What kind of change is this?</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the category that best describes the work.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="feature">New feature</QuestionnaireChoice>
<QuestionnaireChoice value="fix">Bug fix</QuestionnaireChoice>
<QuestionnaireChoice value="refactor">Refactor</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem
name="constraints"
onStatusChange={setConstraintStatus}
>
<QuestionnaireTitle>
Are there any implementation constraints?
</QuestionnaireTitle>
<QuestionnaireDescription>
Answer if needed, or intentionally skip this question.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="no-dependencies">
Do not add dependencies
</QuestionnaireChoice>
<QuestionnaireChoice value="no-migrations">
Do not change the database
</QuestionnaireChoice>
<QuestionnaireChoice value="preserve-api">
Preserve the public API
</QuestionnaireChoice>
<QuestionnaireInput
aria-label="Another implementation constraint"
placeholder="Describe another constraint…"
/>
</QuestionnaireChoices>
</QuestionnaireItem>
<QuestionnaireItem name="review" required>
<QuestionnaireTitle>
How should the work be reviewed?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the checks the agent should complete before handoff.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests">
Run the test suite
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Review the final diff
</QuestionnaireChoice>
<QuestionnaireChoice value="both">
Tests and diff review
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Submit brief</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Shortcuts#
Give each answer a letter or number key with shortcuts. The key shows as a recessed keycap at the end of the choice.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const items = [
{
choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }],
name: "action",
required: true,
},
] as const
type ShortcutMode = React.ComponentProps<typeof Questionnaire>["shortcuts"]
export function QuestionnaireShortcuts() {
const [shortcuts, setShortcuts] = React.useState<ShortcutMode>("letters")
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const action = new FormData(event.currentTarget).get("action")
toast("Next action selected", {
description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts ?? "none"}`,
})
}
return (
<div className="mx-auto flex h-full w-full max-w-md flex-col gap-8">
<ToggleGroup
aria-label="Shortcut style"
className="self-end"
size="sm"
spacing={0}
value={[shortcuts ?? "none"]}
onValueChange={(value) => {
const next = value[0]
if (!next) {
return
}
setShortcuts(next === "none" ? undefined : (next as ShortcutMode))
}}
>
<ToggleGroupItem value="none">None</ToggleGroupItem>
<ToggleGroupItem value="letters">Letters</ToggleGroupItem>
<ToggleGroupItem value="numbers">Numbers</ToggleGroupItem>
</ToggleGroup>
<Questionnaire
items={items}
shortcuts={shortcuts}
onSubmit={handleSubmit}
>
<QuestionnaireItem name="action" required>
<QuestionnaireTitle>
What should the agent do next?
</QuestionnaireTitle>
<QuestionnaireDescription>
Use the displayed shortcut or navigate with the keyboard.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="inspect">
Inspect the implementation
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Run the relevant tests
</QuestionnaireChoice>
<QuestionnaireChoice value="patch">
Prepare the patch
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>Confirm action</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</div>
)
}Custom Validation#
Combine controlled navigation with your own validation (or a schema library such as Zod) to return to an invalid item and show its error.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Card,
CardAction,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "detail", required: true },
{ name: "audience", required: true },
] as const
type QuestionnaireItemName = "detail" | "audience"
type QuestionnaireErrors = Partial<Record<QuestionnaireItemName, string>>
// Swap this for a schema library such as Zod; it only has to return the
// first message for each invalid item, in item order.
function validate(answers: Record<string, FormDataEntryValue>) {
const errors: QuestionnaireErrors = {}
if (answers.detail !== "summary" && answers.detail !== "complete") {
errors.detail = "Choose the response depth."
}
if (answers.audience !== "team" && answers.audience !== "public") {
errors.audience = "Choose who will read the answer."
}
if (answers.audience === "public" && answers.detail === "summary") {
errors.detail =
"Public answers need enough context. Choose a complete answer."
}
return errors
}
function ValidationProgress() {
return (
<QuestionnaireProgress
className="min-w-0"
render={(props, state) => (
<div {...props}>
{state.current} / {state.total}
</div>
)}
/>
)
}
export function QuestionnaireValidation() {
const [item, setItem] = React.useState("detail")
const [errors, setErrors] = React.useState<QuestionnaireErrors>({})
function clearError(name: QuestionnaireItemName) {
setErrors((currentErrors) => {
if (!currentErrors[name]) {
return currentErrors
}
const nextErrors = { ...currentErrors }
delete nextErrors[name]
return nextErrors
})
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const answers = Object.fromEntries(new FormData(event.currentTarget))
const nextErrors = validate(answers)
const firstInvalidItem = items.find((item) => nextErrors[item.name])
setErrors(nextErrors)
if (firstInvalidItem) {
setItem(firstInvalidItem.name)
return
}
toast("Agent response configured", {
description: `Detail: ${answers.detail} · Audience: ${answers.audience}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
item={item}
items={items}
onItemChange={setItem}
onSubmit={handleSubmit}
>
<Card className="w-full">
<QuestionnaireItem
invalid={Boolean(errors.detail)}
name="detail"
required
>
<CardHeader>
<QuestionnaireTitle>
How much detail should the answer include?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the response depth.
</QuestionnaireDescription>
<CardAction>
<ValidationProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice
value="summary"
onChange={() => clearError("detail")}
>
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice
value="complete"
onChange={() => clearError("detail")}
>
Complete answer
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError>{errors.detail}</QuestionnaireError>
</CardContent>
</QuestionnaireItem>
<QuestionnaireItem
invalid={Boolean(errors.audience)}
name="audience"
required
>
<CardHeader>
<QuestionnaireTitle>Who will read the answer?</QuestionnaireTitle>
<QuestionnaireDescription>
Public answers require complete context.
</QuestionnaireDescription>
<CardAction>
<ValidationProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice
value="team"
onChange={() => clearError("audience")}
>
My team
</QuestionnaireChoice>
<QuestionnaireChoice
value="public"
onChange={() => clearError("audience")}
>
Public audience
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError>{errors.audience}</QuestionnaireError>
</CardContent>
</QuestionnaireItem>
<CardFooter>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Validate answers</QuestionnaireSubmit>
</QuestionnaireActions>
</CardFooter>
</Card>
</Questionnaire>
)
}Controlled#
Control the active item from your own state.
Current checkpoint: Change scope
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "scope", required: true },
{ name: "checks", required: true },
{ name: "output", required: true },
] as const
const itemLabels: Record<string, string> = {
scope: "Change scope",
checks: "Verification",
output: "Final output",
}
export function QuestionnaireControlled() {
const [item, setItem] = React.useState("scope")
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Agent workflow configured", {
description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("checks") ?? "None"} · Output: ${formData.get("output") ?? "None"}`,
})
}
return (
<div className="relative mx-auto flex h-full w-full max-w-md flex-col">
<p
className="absolute end-0 top-0 text-sm text-muted-foreground"
role="status"
>
Current checkpoint: {itemLabels[item]}
</p>
<Questionnaire
className="mt-auto"
item={item}
items={items}
onItemChange={setItem}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem name="scope" required>
<QuestionnaireTitle>What may the agent change?</QuestionnaireTitle>
<QuestionnaireDescription>
The host stores the active checkpoint while Questionnaire navigates.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="component">
Only the target component
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Component and related tests
</QuestionnaireChoice>
<QuestionnaireChoice value="feature">
The complete feature area
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="checks" required>
<QuestionnaireTitle>
Which verification level should it use?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package tests and typecheck
</QuestionnaireChoice>
<QuestionnaireChoice value="full">
Full workspace verification
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="output" required>
<QuestionnaireTitle>
What should the agent return when finished?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Summary with changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="handoff">
Detailed implementation handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Save workflow</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</div>
)
}Resume#
Restore a saved active item and default answers, and reset back to them.
"use client"
import * as React from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "change", required: true },
{ name: "verification", required: true },
{ name: "notes" },
] as const
export function QuestionnaireResume() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const answers = {
change: formData.get("change"),
verification: formData.getAll("verification"),
notes: formData.get("notes"),
}
toast("Draft updated", {
description: `Migration: ${answers.change ?? "None"} · Verification: ${answers.verification.join(", ") || "None"} · Notes: ${answers.notes || "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="verification"
items={items}
onReset={() => toast("Saved answers restored")}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem name="change" required>
<QuestionnaireTitle>What kind of migration is this?</QuestionnaireTitle>
<QuestionnaireDescription>
This answer was saved during the previous session.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="incremental" defaultChecked>
Incremental migration
</QuestionnaireChoice>
<QuestionnaireChoice value="cutover">
Single cutover
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="verification" multiple required>
<QuestionnaireTitle>
How should the migration be verified?
</QuestionnaireTitle>
<QuestionnaireDescription>
These checks were selected during the previous session.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests" defaultChecked>
Run migration tests
</QuestionnaireChoice>
<QuestionnaireChoice value="typecheck" defaultChecked>
Run the typecheck
</QuestionnaireChoice>
<QuestionnaireChoice value="manual">
Perform a manual smoke test
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="notes">
<QuestionnaireTitle>
Anything else the agent should remember?
</QuestionnaireTitle>
<QuestionnaireDescription>
This note was saved with the draft.
</QuestionnaireDescription>
<QuestionnaireInput
aria-label="Saved migration note"
defaultValue="Keep the existing public API stable."
/>
</QuestionnaireItem>
<QuestionnaireActions>
<Button type="reset" variant="outline">
Reset changes
</Button>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Update draft</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Conditional Items#
Disable items that don't apply to earlier answers.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
export function QuestionnaireConditional() {
const [runtime, setRuntime] = React.useState("local")
const items = React.useMemo(
() => [
{ name: "runtime", required: true },
{
disabled: runtime !== "cloud",
name: "environment",
required: true,
},
{ name: "approval", required: true },
],
[runtime]
)
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Execution plan saved", {
description: `Runtime: ${formData.get("runtime") ?? "None"} · Environment: ${formData.get("environment") ?? "Not applicable"} · Approval: ${formData.get("approval") ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="runtime"
items={items}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem name="runtime" required>
<QuestionnaireTitle>Where should the agent run?</QuestionnaireTitle>
<QuestionnaireDescription>
Cloud runs add an environment question to this flow.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice
checked={runtime === "local"}
value="local"
onChange={() => setRuntime("local")}
>
Local workspace
</QuestionnaireChoice>
<QuestionnaireChoice
checked={runtime === "cloud"}
value="cloud"
onChange={() => setRuntime("cloud")}
>
Cloud workspace
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem
disabled={runtime !== "cloud"}
name="environment"
required
>
<QuestionnaireTitle>
Which cloud environment should it use?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="preview">Preview</QuestionnaireChoice>
<QuestionnaireChoice value="staging">Staging</QuestionnaireChoice>
<QuestionnaireChoice value="isolated">
Isolated sandbox
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="approval" required>
<QuestionnaireTitle>
When should the agent request approval?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="writes">
Before writing files
</QuestionnaireChoice>
<QuestionnaireChoice value="commands">
Before running commands
</QuestionnaireChoice>
<QuestionnaireChoice value="sensitive">
Only for sensitive actions
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Save execution plan</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Navigation State#
Read an item's status to disable navigation or restyle the actions.
"use client"
import * as React from "react"
import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "permission", required: true },
{ name: "verification", required: true },
] as const
type ItemName = "permission" | "verification"
export function QuestionnaireNavigationState() {
const [item, setItem] = React.useState<ItemName>("permission")
const [statuses, setStatuses] = React.useState<
Record<ItemName, QuestionnaireItemStatus>
>({
permission: "unanswered",
verification: "unanswered",
})
const unanswered = statuses[item] === "unanswered"
function setStatus(name: ItemName, status: QuestionnaireItemStatus) {
setStatuses((current) => ({ ...current, [name]: status }))
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Permissions saved", {
description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
item={item}
items={items}
onItemChange={(nextItem) => setItem(nextItem as ItemName)}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem
name="permission"
required
onStatusChange={(status) => setStatus("permission", status)}
>
<QuestionnaireTitle>What may the agent modify?</QuestionnaireTitle>
<QuestionnaireDescription>
Next is intentionally disabled until an answer is selected.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="files">Project files</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Project files and tests
</QuestionnaireChoice>
<QuestionnaireChoice value="config">
Files, tests, and configuration
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem
name="verification"
required
onStatusChange={(status) => setStatus("verification", status)}
>
<QuestionnaireTitle>
What must pass before completion?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests">Tests</QuestionnaireChoice>
<QuestionnaireChoice value="types">
Tests and types
</QuestionnaireChoice>
<QuestionnaireChoice value="all">
Tests, types, and visual QA
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext
className="data-[status=unanswered]:opacity-50"
disabled={unanswered}
variant="secondary"
>
Next
</QuestionnaireNext>
<QuestionnaireSubmit disabled={unanswered}>
Save permissions
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Custom Progress#
Use the Progress render state to build your own indicator.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "scope", required: true },
{ name: "strategy", required: true },
{ name: "tests", required: true },
{ name: "delivery", required: true },
] as const
export function QuestionnaireProgressExample() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Pull request plan ready", {
description: `Scope: ${formData.get("scope") ?? "None"} · Commits: ${formData.get("strategy") ?? "None"} · Tests: ${formData.get("tests") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="scope"
items={items}
onSubmit={handleSubmit}
>
<QuestionnaireProgress
className="w-full"
render={(props, state) => (
<div {...props}>
<div className="mb-2 flex gap-1.5" aria-hidden="true">
{Array.from({ length: state.total }, (_, index) => (
<span
key={index}
className={
index < state.current
? "h-1.5 flex-1 rounded-full bg-primary"
: "h-1.5 flex-1 rounded-full bg-muted"
}
/>
))}
</div>
<span>
Checkpoint {state.current} of {state.total}
</span>
</div>
)}
/>
<QuestionnaireItem name="scope" required>
<QuestionnaireTitle>How large is the change?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="small">Small patch</QuestionnaireChoice>
<QuestionnaireChoice value="medium">
Feature-sized change
</QuestionnaireChoice>
<QuestionnaireChoice value="large">
Cross-package change
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="strategy" required>
<QuestionnaireTitle>
How should commits be organized?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="single">
Single commit
</QuestionnaireChoice>
<QuestionnaireChoice value="logical">
Logical commits
</QuestionnaireChoice>
<QuestionnaireChoice value="squash">
Squash before review
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="tests" required>
<QuestionnaireTitle>Which tests should run?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package suite
</QuestionnaireChoice>
<QuestionnaireChoice value="workspace">
Full workspace
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="delivery" required>
<QuestionnaireTitle>
How should the work be delivered?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="patch">Patch only</QuestionnaireChoice>
<QuestionnaireChoice value="commit">
Committed locally
</QuestionnaireChoice>
<QuestionnaireChoice value="branch">
Push a review branch
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Finish plan</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Animated Items#
Animate the active item while progress and navigation stay put.
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "task", required: true },
{ name: "review", required: true },
{ name: "delivery", required: true },
] as const
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"
export function QuestionnaireAnimated() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Agent workflow saved", {
description: `Task: ${formData.get("task") ?? "None"} · Review: ${formData.get("review") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="task"
items={items}
onSubmit={handleSubmit}
>
<QuestionnaireProgress />
<QuestionnaireItem className={itemClassName} name="task" required>
<QuestionnaireTitle>What should the agent do?</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the task for this run.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="implement">
Implement the requested change
</QuestionnaireChoice>
<QuestionnaireChoice value="debug">
Debug the current behavior
</QuestionnaireChoice>
<QuestionnaireChoice value="review">
Review the implementation
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem className={itemClassName} name="review" required>
<QuestionnaireTitle>
How should the work be reviewed?
</QuestionnaireTitle>
<QuestionnaireDescription>
Select the verification depth.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted checks
</QuestionnaireChoice>
<QuestionnaireChoice value="complete">
Complete test suite
</QuestionnaireChoice>
<QuestionnaireChoice value="manual">
Tests and manual QA
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem className={itemClassName} name="delivery" required>
<QuestionnaireTitle>
How should the result be delivered?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the final handoff format.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Summary and changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="handoff">
Detailed review handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Save workflow</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
)
}Card#
"use client"
import * as React from "react"
import { toast } from "sonner"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{
choices: [{ value: "fix" }, { value: "refactor" }, { value: "docs" }],
name: "task",
required: true,
},
{
choices: [{ value: "summary" }, { value: "files" }, { value: "review" }],
name: "output",
required: true,
},
] as const
export function QuestionnaireCard() {
const taskTitleId = React.useId()
const outputTitleId = React.useId()
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
toast("Agent task created", {
description: `Task: ${formData.get("task") ?? "None"} · Handoff: ${formData.get("output") ?? "None"}`,
})
}
return (
<Questionnaire
className="mx-auto max-w-md"
defaultItem="task"
items={items}
shortcuts="numbers"
onSubmit={handleSubmit}
>
<Card>
<QuestionnaireItem aria-labelledby={taskTitleId} name="task" required>
<CardHeader>
<QuestionnaireTitle id={taskTitleId} render={<CardTitle />}>
What should the agent work on?
</QuestionnaireTitle>
<QuestionnaireDescription render={<CardDescription />}>
Choose the task that should be handled next.
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="fix">
Fix the failing tests
</QuestionnaireChoice>
<QuestionnaireChoice value="refactor">
Refactor the data layer
</QuestionnaireChoice>
<QuestionnaireChoice value="docs">
Update the integration guide
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</CardContent>
</QuestionnaireItem>
<QuestionnaireItem
aria-labelledby={outputTitleId}
name="output"
required
>
<CardHeader>
<QuestionnaireTitle id={outputTitleId} render={<CardTitle />}>
What should the final handoff include?
</QuestionnaireTitle>
<QuestionnaireDescription render={<CardDescription />}>
Pick the level of detail needed for review.
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Summary only
</QuestionnaireChoice>
<QuestionnaireChoice value="files">
Summary and changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="review">
Full review handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</CardContent>
</QuestionnaireItem>
<CardFooter>
<QuestionnaireActions className="w-full">
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Create task</QuestionnaireSubmit>
</QuestionnaireActions>
</CardFooter>
</Card>
</Questionnaire>
)
}Dialog#
Closing and cancelling stay with the dialog.
"use client"
import * as React from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
{ name: "scope", required: true },
{ name: "tests", required: true },
] as const
export function QuestionnaireDialog() {
const [open, setOpen] = React.useState(false)
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
setOpen(false)
toast("Clarification sent", {
description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("tests") ?? "None"}`,
})
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button variant="outline" />}>
Open clarification
</DialogTrigger>
<DialogContent>
<Questionnaire
defaultItem="scope"
items={items}
onSubmit={handleSubmit}
>
<QuestionnaireItem name="scope" required>
<DialogHeader>
<QuestionnaireProgress />
<QuestionnaireTitle render={<DialogTitle />}>
Which files are in scope?
</QuestionnaireTitle>
<QuestionnaireDescription render={<DialogDescription />}>
Choose how broadly the agent can update the workspace.
</QuestionnaireDescription>
</DialogHeader>
<QuestionnaireChoices>
<QuestionnaireChoice value="component">
Component only
</QuestionnaireChoice>
<QuestionnaireChoice value="feature">
Complete feature directory
</QuestionnaireChoice>
<QuestionnaireChoice value="workspace">
Any related workspace file
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="tests" required>
<DialogHeader>
<QuestionnaireProgress />
<QuestionnaireTitle render={<DialogTitle />}>
How much verification is needed?
</QuestionnaireTitle>
<QuestionnaireDescription render={<DialogDescription />}>
Choose the checks the agent should run before handoff.
</QuestionnaireDescription>
</DialogHeader>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package tests
</QuestionnaireChoice>
<QuestionnaireChoice value="full">
Full workspace verification
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>
Cancel
</DialogClose>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>Next</QuestionnaireNext>
<QuestionnaireSubmit>Send answer</QuestionnaireSubmit>
</QuestionnaireActions>
</DialogFooter>
</Questionnaire>
</DialogContent>
</Dialog>
)
}Accessibility#
QuestionnaireItemis afieldsetandQuestionnaireTitleitslegend. The description and the active error are associated with the item; invalid items and answers exposearia-invalid.- Choices are native radios and checkboxes laid over the card, so arrow keys, Space and form submission work as usual. Focus shows the input ring on the card.
- Progress is a named progressbar, navigation uses real buttons, and inactive items and actions are hidden and inert. Moving to an item focuses it; failed validation focuses an answer.
- Give
QuestionnaireInputa visible label,aria-labeloraria-labelledby. A placeholder is not a label.
API Reference#
| Part | Element | Props / notes |
|---|---|---|
Questionnaire | form | items, item / defaultItem / onItemChange, shortcuts: letters · numbers, plus form props such as onSubmit. |
QuestionnaireProgress | div | render with { current, total, first, last }. Defaults to "1 of 3". |
QuestionnaireItem | fieldset | name, required, multiple, invalid, disabled, onStatusChange. Exposes data-status: unanswered · answered · skipped. |
QuestionnaireTitle | legend | The question, 16/20 Medium. |
QuestionnaireDescription | p | 4px below the title. |
QuestionnaireChoices | div | The choice list, 8px apart. |
QuestionnaireChoice | label | value, checked / defaultChecked, disabled, onChange. Exposes data-checked, data-type, data-shortcut. |
QuestionnaireChoiceDescription | span | A muted-foreground line under the choice label. |
QuestionnaireInput | input | type and input props. Needs an accessible name. |
QuestionnaireError | p | The item's error, 6px under the choices. |
QuestionnaireActions | div | Lays out the navigation: Previous at the start, Skip and Next or Submit at the end. |
QuestionnairePrevious | button | Button variant (default outline) and size. |
QuestionnaireSkip | button | Button variant (default outline) and size. |
QuestionnaireNext | button | Button variant (default default) and size. |
QuestionnaireSubmit | button | Button variant (default default) and size. |