Questionnaire
The Questionnaire block renders a multi-step question flow as a single form. Use it for agent clarification prompts, onboarding, surveys, intake forms, and configuration.
import { For, Show } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
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 default function QuestionnaireDemo() { function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="direction" items={questionnaireItems} shortcuts="letters" onSubmit={handleSubmit} > <Questionnaire.Progress /> <For each={questionnaireItems}> {(question) => ( <Questionnaire.Item multiple={"multiple" in question && question.multiple} name={question.name} required={question.required} > <Questionnaire.Title>{question.title}</Questionnaire.Title> <Questionnaire.Description>{question.description}</Questionnaire.Description> <Questionnaire.Choices> <For each={question.choices}> {(choice) => ( <Questionnaire.Choice value={choice.value}> <span class="font-medium">{choice.label}</span> <Show when={"description" in choice ? choice.description : undefined}> {(description) => ( <span class="text-muted-foreground">{description()}</span> )} </Show> </Questionnaire.Choice> )} </For> <Show when={"input" in question ? question.input : undefined}> {(input) => ( <Questionnaire.Input aria-label={input().label} placeholder={input().placeholder} /> )} </Show> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item> )} </For> <Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Skip /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Save plan</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Installation
CLI
Manual
Install the following dependency:
Copy the questionnaire folder into your project.
import { QuestionnaireActions, QuestionnaireChoice, QuestionnaireChoiceDescription, QuestionnaireChoiceInput, QuestionnaireChoiceLabel, QuestionnaireChoiceShortcut, QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError, QuestionnaireInput, QuestionnaireItem, QuestionnaireNext, QuestionnairePrevious, QuestionnaireProgress, QuestionnaireRoot, QuestionnaireSkip, QuestionnaireSubmit, QuestionnaireTitle,} from "./components";import { useQuestionnaire } from "./context";
const Questionnaire = { Root: QuestionnaireRoot, Progress: QuestionnaireProgress, Item: QuestionnaireItem, Title: QuestionnaireTitle, Description: QuestionnaireDescription, Choices: QuestionnaireChoices, Choice: QuestionnaireChoice, ChoiceDescription: QuestionnaireChoiceDescription, Input: QuestionnaireInput, Error: QuestionnaireError, Actions: QuestionnaireActions, Previous: QuestionnairePrevious, Skip: QuestionnaireSkip, Next: QuestionnaireNext, Submit: QuestionnaireSubmit, // Headless choice sub-parts for custom compositions. ChoiceInput: QuestionnaireChoiceInput, ChoiceLabel: QuestionnaireChoiceLabel, ChoiceShortcut: QuestionnaireChoiceShortcut,};
export type { QuestionnaireChoiceDefinition, QuestionnaireInputType, QuestionnaireItemDefinition, QuestionnaireItemStatus, QuestionnaireRootState, QuestionnaireShortcutMode,} from "./types";
export { Questionnaire, useQuestionnaire };Usage
import { Questionnaire } from "~/components/blocks/questionnaire";const items = [ { name: "direction", required: true, prompt: "What should we prototype next?", description: "Choose a direction or write your own.", choices: [ { value: "delegation", label: "Delegation", description: "Show how work moves to a specialist.", }, { value: "questions", label: "Question prompts", description: "Show choices while the interface waits.", }, { value: "both", label: "Both together" }, ], input: { label: "Another answer", placeholder: "Type another answer…" }, }, { name: "detail", required: false, prompt: "How much detail should it include?", description: "Skip this if you are not sure yet.", choices: [ { value: "focused", label: "Focused" }, { value: "complete", label: "Complete flow" }, ], },] as const;Define the collection once: pass it to Questionnaire.Root for progress, actions, and shortcuts,
then map it into the parts.
<Questionnaire.Root items={items} onSubmit={handleSubmit}> <Questionnaire.Progress /> <For each={items}> {(question) => ( <Questionnaire.Item name={question.name} required={question.required}> <Questionnaire.Title>{question.prompt}</Questionnaire.Title> <Questionnaire.Description>{question.description}</Questionnaire.Description> <Questionnaire.Choices> <For each={question.choices}> {(choice) => ( <Questionnaire.Choice value={choice.value}> <span class="font-medium">{choice.label}</span> <Show when={"description" in choice ? choice.description : undefined}> <span class="text-muted-foreground">{choice.description}</span> </Show> </Questionnaire.Choice> )} </For> <Show when={"input" in question ? question.input : undefined}> {(input) => ( <Questionnaire.Input aria-label={input().label} placeholder={input().placeholder} /> )} </Show> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item> )} </For> <Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Skip /> <Questionnaire.Next /> <Questionnaire.Submit /> </Questionnaire.Actions></Questionnaire.Root>The answers submit as regular form data:
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault(); const answers = new FormData(event.currentTarget); // answers.get("direction"), answers.getAll(...) for multiple items.}Composition
Use the following composition to build a questionnaire:
Questionnaire.Root├── Questionnaire.Progress├── Questionnaire.Item│ ├── Questionnaire.Title│ ├── Questionnaire.Description│ ├── Questionnaire.Choices│ │ ├── Questionnaire.Choice│ │ │ └── Questionnaire.ChoiceDescription│ │ └── Questionnaire.Input│ └── Questionnaire.Error└── Questionnaire.Actions ├── Questionnaire.Previous ├── Questionnaire.Skip ├── Questionnaire.Next └── Questionnaire.SubmitQuestionnaire.Root owns the ordered items, active item, answer state, validation, progress, and
navigation. The containing page, card, dialog, or drawer owns close and cancellation behavior,
persistence, transport, and branching.
Questionnaire.Choice renders its native input, indicator, label, and shortcut parts by default.
The Questionnaire.ChoiceInput, Questionnaire.ChoiceLabel, and Questionnaire.ChoiceShortcut
sub-parts are also exported for custom compositions that read the choice state.
Item Definitions
The items prop on Questionnaire.Root is the collection of record. It drives the progress
count, decides when Previous, Skip, Next, and Submit are visible, and assigns shortcut keys in
definition order, independently of when the rendered parts mount.
Keep the definitions and the rendered Questionnaire.Item and Questionnaire.Choice elements in
sync: name, required, disabled, and choice value order must match. In development the
block logs a console warning for every drift it detects, such as an item defined but never
rendered or a choice missing from the definitions.
Multiple Selection
Use multiple for an item that accepts more than one fixed answer.
import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { choices: [{ value: "source" }, { value: "tests" }, { value: "docs" }, { value: "history" }], name: "context", required: true, },] as const;
export default function QuestionnaireMultiple() { function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const context = new FormData(event.currentTarget).getAll("context");
toast("Context selected", { description: `Context: ${context.join(", ") || "None"}`, }); }
return ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" items={items} shortcuts="letters" onSubmit={handleSubmit} > <Questionnaire.Item name="context" multiple required> <Questionnaire.Title>What context should the agent inspect?</Questionnaire.Title> <Questionnaire.Description> Select every source that may affect the implementation. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="source">Relevant source files</Questionnaire.Choice> <Questionnaire.Choice value="tests">Existing tests</Questionnaire.Choice> <Questionnaire.Choice value="docs">Architecture documentation</Questionnaire.Choice> <Questionnaire.Choice value="history">Recent commit history</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Submit>Share context</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Freeform Answer
Compose Questionnaire.Input with fixed choices when the user can provide another answer.
import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { choices: [{ value: "incremental" }, { value: "module" }, { value: "rewrite" }], name: "approach", required: true, },] as const;
export default function QuestionnaireFreeform() { function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const approach = new FormData(event.currentTarget).get("approach");
toast("Approach selected", { description: `Approach: ${approach ?? "None"}`, }); }
return ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" items={items} shortcuts="letters" onSubmit={handleSubmit} > <Questionnaire.Item name="approach" required> <Questionnaire.Title>How should the agent approach this refactor?</Questionnaire.Title> <Questionnaire.Description> Choose a strategy or write a more specific instruction. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="incremental"> Make the smallest safe change </Questionnaire.Choice> <Questionnaire.Choice value="module"> Refactor one module at a time </Questionnaire.Choice> <Questionnaire.Choice value="rewrite"> Replace the implementation completely </Questionnaire.Choice> <Questionnaire.Input aria-label="Another refactoring approach" placeholder="Describe another approach…" /> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Submit>Use this approach</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Explicit Skip
Add Questionnaire.Skip when an optional item may be intentionally left unanswered.
import { createSignal } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire, type QuestionnaireItemStatus,} from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "task", required: true }, { name: "constraints" }, { name: "review", required: true },] as const;
export default function QuestionnaireSkipDemo() { const [constraintStatus, setConstraintStatus] = createSignal<QuestionnaireItemStatus>("unanswered");
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const formData = new FormData(event.currentTarget); const answers = { task: formData.get("task"), constraints: formData.get("constraints"), constraintStatus: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="task" items={items} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item name="task" required> <Questionnaire.Title>What kind of change is this?</Questionnaire.Title> <Questionnaire.Description> Choose the category that best describes the work. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="feature">New feature</Questionnaire.Choice> <Questionnaire.Choice value="fix">Bug fix</Questionnaire.Choice> <Questionnaire.Choice value="refactor">Refactor</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="constraints" onStatusChange={setConstraintStatus}> <Questionnaire.Title>Are there any implementation constraints?</Questionnaire.Title> <Questionnaire.Description> Answer if needed, or intentionally skip this question. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="no-dependencies"> Do not add dependencies </Questionnaire.Choice> <Questionnaire.Choice value="no-migrations"> Do not change the database </Questionnaire.Choice> <Questionnaire.Choice value="preserve-api"> Preserve the public API </Questionnaire.Choice> <Questionnaire.Input aria-label="Another implementation constraint" placeholder="Describe another constraint…" /> </Questionnaire.Choices> </Questionnaire.Item>
<Questionnaire.Item name="review" required> <Questionnaire.Title>How should the work be reviewed?</Questionnaire.Title> <Questionnaire.Description> Choose the checks the agent should complete before handoff. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="tests">Run the test suite</Questionnaire.Choice> <Questionnaire.Choice value="diff">Review the final diff</Questionnaire.Choice> <Questionnaire.Choice value="both">Tests and diff review</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Skip /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Submit brief</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Shortcuts
Assign a letter or number key to each answer with shortcuts.
import { createSignal } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire, type QuestionnaireShortcutMode,} from "@/registry/kobalte/blocks/questionnaire";import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select";import { Toaster } from "~/components/ui/toast";
const items = [ { choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }], name: "action", required: true, },] as const;
export default function QuestionnaireShortcuts() { const [shortcuts, setShortcuts] = createSignal<QuestionnaireShortcutMode | undefined>("letters");
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const action = new FormData(event.currentTarget).get("action");
toast("Next action selected", { description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts() ?? "none"}`, }); }
return ( <div class="relative mx-auto flex h-full w-full max-w-md flex-col"> <Toaster /> <NativeSelect aria-label="Shortcut style" class="absolute end-0 top-0" value={shortcuts() ?? "none"} onChange={(event) => { const value = event.currentTarget.value; setShortcuts(value === "letters" || value === "numbers" ? value : undefined); }} > <NativeSelectOption value="none">No shortcuts</NativeSelectOption> <NativeSelectOption value="letters">Letters</NativeSelectOption> <NativeSelectOption value="numbers">Numbers</NativeSelectOption> </NativeSelect>
<Questionnaire.Root class="mt-auto" items={items} shortcuts={shortcuts()} onSubmit={handleSubmit} > <Questionnaire.Item name="action" required> <Questionnaire.Title>What should the agent do next?</Questionnaire.Title> <Questionnaire.Description> Use the displayed shortcut or navigate with the keyboard. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="inspect">Inspect the implementation</Questionnaire.Choice> <Questionnaire.Choice value="tests">Run the relevant tests</Questionnaire.Choice> <Questionnaire.Choice value="patch">Prepare the patch</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Submit>Confirm action</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </div> );}Custom Validation
Combine controlled navigation with an external schema such as Zod to return to an invalid item and present its error.
import { createSignal } from "solid-js";import { toast } from "solid-sonner";import { z } from "zod";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Card, CardAction, CardContent, CardFooter, CardHeader } from "~/components/ui/card";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "detail", required: true }, { name: "audience", required: true },] as const;
const questionnaireSchema = z .object({ detail: z.enum(["summary", "complete"]), audience: z.enum(["team", "public"]), }) .superRefine((answers, context) => { if (answers.audience === "public" && answers.detail === "summary") { context.addIssue({ code: "custom", message: "Public answers need enough context. Choose a complete answer.", path: ["detail"], }); } });
type QuestionnaireItemName = keyof z.infer<typeof questionnaireSchema>;type QuestionnaireErrors = Partial<Record<QuestionnaireItemName, string>>;
function ValidationProgress() { return ( <Questionnaire.Progress class="min-w-0"> {(state) => ( <> {state.current} / {state.total} </> )} </Questionnaire.Progress> );}
export default function QuestionnaireValidation() { const [item, setItem] = createSignal("detail"); const [errors, setErrors] = createSignal<QuestionnaireErrors>({});
function clearError(name: QuestionnaireItemName) { setErrors((currentErrors) => { if (!currentErrors[name]) { return currentErrors; }
const nextErrors = { ...currentErrors }; delete nextErrors[name]; return nextErrors; }); }
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const result = questionnaireSchema.safeParse( Object.fromEntries(new FormData(event.currentTarget)), );
if (result.success) { setErrors({}); toast("Agent response configured", { description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`, }); return; }
const nextErrors: QuestionnaireErrors = {};
for (const issue of result.error.issues) { const name = issue.path[0];
if ((name === "detail" || name === "audience") && !nextErrors[name]) { nextErrors[name] = issue.message; } }
const firstInvalidItem = result.error.issues[0]?.path[0];
setErrors(nextErrors);
if (firstInvalidItem === "detail" || firstInvalidItem === "audience") { setItem(firstInvalidItem); } }
return ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" item={item()} items={items} onItemChange={setItem} onSubmit={handleSubmit} > <Card class="w-full"> <Questionnaire.Item invalid={Boolean(errors().detail)} name="detail" required> <CardHeader> <Questionnaire.Title>How much detail should the answer include?</Questionnaire.Title> <Questionnaire.Description>Choose the response depth.</Questionnaire.Description> <CardAction> <ValidationProgress /> </CardAction> </CardHeader> <CardContent> <Questionnaire.Choices> <Questionnaire.Choice value="summary" onChange={() => clearError("detail")}> Concise summary </Questionnaire.Choice> <Questionnaire.Choice value="complete" onChange={() => clearError("detail")}> Complete answer </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error>{errors().detail}</Questionnaire.Error> </CardContent> </Questionnaire.Item>
<Questionnaire.Item invalid={Boolean(errors().audience)} name="audience" required> <CardHeader> <Questionnaire.Title>Who will read the answer?</Questionnaire.Title> <Questionnaire.Description> Public answers require complete context. </Questionnaire.Description> <CardAction> <ValidationProgress /> </CardAction> </CardHeader> <CardContent> <Questionnaire.Choices> <Questionnaire.Choice value="team" onChange={() => clearError("audience")}> My team </Questionnaire.Choice> <Questionnaire.Choice value="public" onChange={() => clearError("audience")}> Public audience </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error>{errors().audience}</Questionnaire.Error> </CardContent> </Questionnaire.Item>
<CardFooter> <Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Validate answers</Questionnaire.Submit> </Questionnaire.Actions> </CardFooter> </Card> </Questionnaire.Root> </> );}Controlled
Control the active item from host state, such as returning to an invalid step.
Current checkpoint: Change scope
import { createSignal } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
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 default function QuestionnaireControlled() { const [item, setItem] = createSignal("scope");
function handleSubmit(event: SubmitEvent & { currentTarget: 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 class="relative mx-auto flex h-full w-full max-w-md flex-col"> <Toaster /> <p class="absolute end-0 top-0 text-muted-foreground text-sm" role="status"> Current checkpoint: {itemLabels[item()]} </p>
<Questionnaire.Root class="mt-auto" item={item()} items={items} onItemChange={setItem} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item name="scope" required> <Questionnaire.Title>What may the agent change?</Questionnaire.Title> <Questionnaire.Description> The host stores the active checkpoint while Questionnaire navigates. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="component">Only the target component</Questionnaire.Choice> <Questionnaire.Choice value="tests">Component and related tests</Questionnaire.Choice> <Questionnaire.Choice value="feature">The complete feature area</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="checks" required> <Questionnaire.Title>Which verification level should it use?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice> <Questionnaire.Choice value="package">Package tests and typecheck</Questionnaire.Choice> <Questionnaire.Choice value="full">Full workspace verification</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="output" required> <Questionnaire.Title>What should the agent return when finished?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="summary">Concise summary</Questionnaire.Choice> <Questionnaire.Choice value="diff">Summary with changed files</Questionnaire.Choice> <Questionnaire.Choice value="handoff"> Detailed implementation handoff </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Save workflow</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </div> );}Resume
Restore a saved active item and default answers, then reset changes back to that saved state.
import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Button } from "~/components/ui/button";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "change", required: true }, { name: "verification", required: true }, { name: "notes" },] as const;
export default function QuestionnaireResume() { function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="verification" items={items} onReset={() => toast("Saved answers restored")} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item name="change" required> <Questionnaire.Title>What kind of migration is this?</Questionnaire.Title> <Questionnaire.Description> This answer was saved during the previous session. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="incremental" defaultChecked> Incremental migration </Questionnaire.Choice> <Questionnaire.Choice value="cutover">Single cutover</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="verification" multiple required> <Questionnaire.Title>How should the migration be verified?</Questionnaire.Title> <Questionnaire.Description> These checks were selected during the previous session. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="tests" defaultChecked> Run migration tests </Questionnaire.Choice> <Questionnaire.Choice value="typecheck" defaultChecked> Run the typecheck </Questionnaire.Choice> <Questionnaire.Choice value="manual">Perform a manual smoke test</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="notes"> <Questionnaire.Title>Anything else the agent should remember?</Questionnaire.Title> <Questionnaire.Description>This note was saved with the draft.</Questionnaire.Description> <Questionnaire.Input aria-label="Saved migration note" defaultValue="Keep the existing public API stable." /> </Questionnaire.Item>
<Questionnaire.Actions> <Button type="reset" variant="outline"> Reset changes </Button> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Update draft</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Conditional Items
Disable items that do not apply to the user's earlier answers.
import { createMemo, createSignal } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
export default function QuestionnaireConditional() { const [runtime, setRuntime] = createSignal("local"); const items = createMemo( () => [ { name: "runtime", required: true }, { disabled: runtime() !== "cloud", name: "environment", required: true, }, { name: "approval", required: true }, ] as const, );
function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="runtime" items={items()} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item name="runtime" required> <Questionnaire.Title>Where should the agent run?</Questionnaire.Title> <Questionnaire.Description> Cloud runs add an environment question to this flow. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice checked={runtime() === "local"} value="local" onChange={() => setRuntime("local")} > Local workspace </Questionnaire.Choice> <Questionnaire.Choice checked={runtime() === "cloud"} value="cloud" onChange={() => setRuntime("cloud")} > Cloud workspace </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item disabled={runtime() !== "cloud"} name="environment" required> <Questionnaire.Title>Which cloud environment should it use?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="preview">Preview</Questionnaire.Choice> <Questionnaire.Choice value="staging">Staging</Questionnaire.Choice> <Questionnaire.Choice value="isolated">Isolated sandbox</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="approval" required> <Questionnaire.Title>When should the agent request approval?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="writes">Before writing files</Questionnaire.Choice> <Questionnaire.Choice value="commands">Before running commands</Questionnaire.Choice> <Questionnaire.Choice value="sensitive"> Only for sensitive actions </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Save execution plan</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Navigation State
Read item status to opt into disabled navigation and custom action styling.
import { toast } from "solid-sonner";import { Questionnaire, useQuestionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "permission", required: true }, { name: "verification", required: true },] as const;
function NavigationActions() { const state = useQuestionnaire(); const unanswered = () => state.activeItemStatus === "unanswered";
return ( <Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next class="data-[status=unanswered]:opacity-50" disabled={unanswered()} variant="secondary" > Next ({state.current} of {state.total}) </Questionnaire.Next> <Questionnaire.Submit disabled={unanswered()}>Save permissions</Questionnaire.Submit> </Questionnaire.Actions> );}
export default function QuestionnaireNavigationState() { function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) { event.preventDefault();
const formData = new FormData(event.currentTarget);
toast("Permissions saved", { description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`, }); }
return ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="permission" items={items} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item name="permission" required> <Questionnaire.Title>What may the agent modify?</Questionnaire.Title> <Questionnaire.Description> Next is disabled until useQuestionnaire() reports the active item as answered. </Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="files">Project files</Questionnaire.Choice> <Questionnaire.Choice value="tests">Project files and tests</Questionnaire.Choice> <Questionnaire.Choice value="config"> Files, tests, and configuration </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="verification" required> <Questionnaire.Title>What must pass before completion?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="tests">Tests</Questionnaire.Choice> <Questionnaire.Choice value="types">Tests and types</Questionnaire.Choice> <Questionnaire.Choice value="all">Tests, types, and visual QA</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<NavigationActions /> </Questionnaire.Root> </> );}Custom Progress
Pass a function child to Questionnaire.Progress to build a custom progress indicator from the
root state.
import { Index } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "scope", required: true }, { name: "strategy", required: true }, { name: "tests", required: true }, { name: "delivery", required: true },] as const;
export default function QuestionnaireProgressDemo() { function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="scope" items={items} onSubmit={handleSubmit} > <Questionnaire.Progress class="w-full"> {(state) => ( <> <div class="mb-2 flex gap-1.5" aria-hidden="true"> <Index each={Array.from({ length: state.total })}> {(_, index) => ( <span class={ index < state.current ? "h-1.5 flex-1 rounded-full bg-primary" : "h-1.5 flex-1 rounded-full bg-muted" } /> )} </Index> </div> <span> Checkpoint {state.current} of {state.total} </span> </> )} </Questionnaire.Progress>
<Questionnaire.Item name="scope" required> <Questionnaire.Title>How large is the change?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="small">Small patch</Questionnaire.Choice> <Questionnaire.Choice value="medium">Feature-sized change</Questionnaire.Choice> <Questionnaire.Choice value="large">Cross-package change</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="strategy" required> <Questionnaire.Title>How should commits be organized?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="single">Single commit</Questionnaire.Choice> <Questionnaire.Choice value="logical">Logical commits</Questionnaire.Choice> <Questionnaire.Choice value="squash">Squash before review</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="tests" required> <Questionnaire.Title>Which tests should run?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice> <Questionnaire.Choice value="package">Package suite</Questionnaire.Choice> <Questionnaire.Choice value="workspace">Full workspace</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="delivery" required> <Questionnaire.Title>How should the work be delivered?</Questionnaire.Title> <Questionnaire.Choices> <Questionnaire.Choice value="patch">Patch only</Questionnaire.Choice> <Questionnaire.Choice value="commit">Committed locally</Questionnaire.Choice> <Questionnaire.Choice value="branch">Push a review branch</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Finish plan</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Animated Items
Animate the active item while keeping progress and navigation stationary.
import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "task", required: true }, { name: "review", required: true }, { name: "delivery", required: true },] as const;
const itemClass = "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 default function QuestionnaireAnimated() { function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="task" items={items} onSubmit={handleSubmit} > <Questionnaire.Progress />
<Questionnaire.Item class={itemClass} name="task" required> <Questionnaire.Title>What should the agent do?</Questionnaire.Title> <Questionnaire.Description>Choose the task for this run.</Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="implement"> Implement the requested change </Questionnaire.Choice> <Questionnaire.Choice value="debug">Debug the current behavior</Questionnaire.Choice> <Questionnaire.Choice value="review">Review the implementation</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item class={itemClass} name="review" required> <Questionnaire.Title>How should the work be reviewed?</Questionnaire.Title> <Questionnaire.Description>Select the verification depth.</Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="targeted">Targeted checks</Questionnaire.Choice> <Questionnaire.Choice value="complete">Complete test suite</Questionnaire.Choice> <Questionnaire.Choice value="manual">Tests and manual QA</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item class={itemClass} name="delivery" required> <Questionnaire.Title>How should the result be delivered?</Questionnaire.Title> <Questionnaire.Description>Choose the final handoff format.</Questionnaire.Description> <Questionnaire.Choices> <Questionnaire.Choice value="summary">Concise summary</Questionnaire.Choice> <Questionnaire.Choice value="diff">Summary and changed files</Questionnaire.Choice> <Questionnaire.Choice value="handoff">Detailed review handoff</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Save workflow</Questionnaire.Submit> </Questionnaire.Actions> </Questionnaire.Root> </> );}Card
Compose Questionnaire with Card slots while keeping the question title and description semantic.
import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Card, CardAction, CardContent, CardFooter, CardHeader } from "~/components/ui/card";import { Toaster } from "~/components/ui/toast";
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 default function QuestionnaireCard() { function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Questionnaire.Root class="mx-auto max-w-md" defaultItem="task" items={items} shortcuts="numbers" onSubmit={handleSubmit} > <Card> <Questionnaire.Item name="task" required> <CardHeader> <Questionnaire.Title class="z-card-title z-font-heading"> What should the agent work on? </Questionnaire.Title> <Questionnaire.Description class="z-card-description"> Choose the task that should be handled next. </Questionnaire.Description> <CardAction> <Questionnaire.Progress /> </CardAction> </CardHeader> <CardContent> <Questionnaire.Choices> <Questionnaire.Choice value="fix">Fix the failing tests</Questionnaire.Choice> <Questionnaire.Choice value="refactor"> Refactor the data layer </Questionnaire.Choice> <Questionnaire.Choice value="docs"> Update the integration guide </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </CardContent> </Questionnaire.Item>
<Questionnaire.Item name="output" required> <CardHeader> <Questionnaire.Title class="z-card-title z-font-heading"> What should the final handoff include? </Questionnaire.Title> <Questionnaire.Description class="z-card-description"> Pick the level of detail needed for review. </Questionnaire.Description> <CardAction> <Questionnaire.Progress /> </CardAction> </CardHeader> <CardContent> <Questionnaire.Choices> <Questionnaire.Choice value="summary">Summary only</Questionnaire.Choice> <Questionnaire.Choice value="files">Summary and changed files</Questionnaire.Choice> <Questionnaire.Choice value="review">Full review handoff</Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </CardContent> </Questionnaire.Item>
<CardFooter> <Questionnaire.Actions class="w-full"> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Create task</Questionnaire.Submit> </Questionnaire.Actions> </CardFooter> </Card> </Questionnaire.Root> </> );}Dialog
Compose Questionnaire inside a Dialog while keeping cancellation and dismissal host-owned.
import { createSignal } from "solid-js";import { toast } from "solid-sonner";import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";import { Button } from "~/components/ui/button";import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTrigger,} from "~/components/ui/dialog";import { Toaster } from "~/components/ui/toast";
const items = [ { name: "scope", required: true }, { name: "tests", required: true },] as const;
export default function QuestionnaireDialog() { const [open, setOpen] = createSignal(false);
function handleSubmit(event: SubmitEvent & { currentTarget: 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 ( <> <Toaster /> <Dialog open={open()} onOpenChange={setOpen}> <DialogTrigger as={Button} variant="outline"> Open clarification </DialogTrigger> <DialogContent> <Questionnaire.Root defaultItem="scope" items={items} onSubmit={handleSubmit}> <Questionnaire.Item name="scope" required> <DialogHeader> <Questionnaire.Progress /> <Questionnaire.Title class="z-dialog-title z-font-heading"> Which files are in scope? </Questionnaire.Title> <Questionnaire.Description class="z-dialog-description"> Choose how broadly the agent can update the workspace. </Questionnaire.Description> </DialogHeader> <Questionnaire.Choices> <Questionnaire.Choice value="component">Component only</Questionnaire.Choice> <Questionnaire.Choice value="feature"> Complete feature directory </Questionnaire.Choice> <Questionnaire.Choice value="workspace"> Any related workspace file </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<Questionnaire.Item name="tests" required> <DialogHeader> <Questionnaire.Progress /> <Questionnaire.Title class="z-dialog-title z-font-heading"> How much verification is needed? </Questionnaire.Title> <Questionnaire.Description class="z-dialog-description"> Choose the checks the agent should run before handoff. </Questionnaire.Description> </DialogHeader> <Questionnaire.Choices> <Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice> <Questionnaire.Choice value="package">Package tests</Questionnaire.Choice> <Questionnaire.Choice value="full"> Full workspace verification </Questionnaire.Choice> </Questionnaire.Choices> <Questionnaire.Error /> </Questionnaire.Item>
<DialogFooter> <DialogClose as={Button} type="button" variant="outline"> Cancel </DialogClose> <Questionnaire.Actions> <Questionnaire.Previous /> <Questionnaire.Next>Next</Questionnaire.Next> <Questionnaire.Submit>Send answer</Questionnaire.Submit> </Questionnaire.Actions> </DialogFooter> </Questionnaire.Root> </DialogContent> </Dialog> </> );}Keyboard
- Enter on a filled answer confirms the current item; ⌘/Ctrl + Enter confirms from anywhere in the form.
- ArrowUp and ArrowDown move focus between answers of the active item.
- ArrowLeft and ArrowRight move to the previous or next item once the current item is answered.
- With
shortcutsenabled, single letter or number keys select the matching choice.
Accessibility
Questionnaire.Item renders a fieldset, and Questionnaire.Title renders its legend.
Descriptions and active errors are associated with the current item, and invalid items and answer
controls expose aria-invalid.
Fixed choices preserve native radio and checkbox behavior. Progress is exposed as a named progressbar, navigation uses real buttons, and inactive items and actions are hidden and inert. Successful navigation focuses the newly active item; failed validation focuses an available answer control.
Always give Questionnaire.Input an accessible name with a visible label, aria-label, or
aria-labelledby. A placeholder is not a label.
API Reference
The state and behavior are implemented in the block itself: every part exposes its state
through data-* attributes such as data-active, data-status, data-checked, data-filled,
and data-visible. Navigation components also accept Button size and variant props, and
Questionnaire.Actions is a styled-only layout helper. Use the exported useQuestionnaire
accessor inside a Questionnaire.Root to read current, total, first, last, and
activeItemStatus for custom composition.