ZaidanHomeDocsComponentsBlocksChartsTypesetCreate

Command Palette

Search for a command to run...

GitHub
New
Sections
  • Introduction
  • Components
  • Blocks
  • Installation
  • Customization
  • Dark Mode
  • Typeset
  • Zaidan Skills
  • FAQ
  • Roadmap
  • Changelog
Installation
  • Astro
  • Manual
  • Solid Start
  • TanStack Router
  • TanStack Start
  • Vite
Blocks
  • Data Grid
  • Event Calendar
  • Filters
  • Gantt
  • Image Crop
  • Kanban
  • Message Scroller
  • Questionnaire
  • Sortable
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Marker
  • Menubar
  • Message
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip

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.

Question 1 of 3
What should the agent build next?

Choose a direction or describe another task.

Choose an answer to continue.

What should every progress update include?

Select all that apply, or skip this question.

Choose an answer or skip this question.

When should work begin?

Choose when the agent should begin the work.

Choose an answer to continue.

1
import { For, Show } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
4
import { Toaster } from "~/components/ui/toast";
5
6
const questionnaireItems = [
7
{
8
choices: [
9
{
10
description: "Show what the agent ran and what came back.",
11
label: "Tool call timeline",
12
value: "tool-calls",
13
},
14
{
15
description: "Ask before sensitive or destructive actions.",
16
label: "Approval checkpoints",
17
value: "approvals",
18
},
19
{
20
description: "Make delegated work and results easier to follow.",
21
label: "Sub-agent handoffs",
22
value: "handoffs",
23
},
24
],
25
description: "Choose a direction or describe another task.",
26
input: {
27
label: "Another agent feature",
28
placeholder: "Describe another feature…",
29
},
30
name: "direction",
31
required: true,
32
title: "What should the agent build next?",
33
},
34
{
35
choices: [
36
{ label: "Progress", value: "progress" },
37
{ label: "Decisions", value: "decisions" },
38
{ label: "Risks", value: "risks" },
39
{ label: "Next step", value: "next-step" },
40
],
41
description: "Select all that apply, or skip this question.",
42
multiple: true,
43
name: "signals",
44
required: false,
45
title: "What should every progress update include?",
46
},
47
{
48
choices: [
49
{ label: "Start now", value: "now" },
50
{ label: "Next development cycle", value: "next-cycle" },
51
{ label: "Add it to the backlog", value: "backlog" },
52
],
53
description: "Choose when the agent should begin the work.",
54
name: "timing",
55
required: true,
56
title: "When should work begin?",
57
},
58
] as const;
59
60
export default function QuestionnaireDemo() {
61
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
62
event.preventDefault();
63
64
const formData = new FormData(event.currentTarget);
65
const answers = {
66
direction: formData.get("direction"),
67
signals: formData.getAll("signals"),
68
timing: formData.get("timing"),
69
};
70
71
toast("Agent plan saved", {
72
description: `Direction: ${answers.direction ?? "None"} · Progress signals: ${answers.signals.join(", ") || "None"} · Timing: ${answers.timing ?? "None"}`,
73
});
74
}
75
76
return (
77
<>
78
<Toaster />
79
<Questionnaire.Root
80
class="mx-auto max-w-md"
81
defaultItem="direction"
82
items={questionnaireItems}
83
shortcuts="letters"
84
onSubmit={handleSubmit}
85
>
86
<Questionnaire.Progress />
87
<For each={questionnaireItems}>
88
{(question) => (
89
<Questionnaire.Item
90
multiple={"multiple" in question && question.multiple}
91
name={question.name}
92
required={question.required}
93
>
94
<Questionnaire.Title>{question.title}</Questionnaire.Title>
95
<Questionnaire.Description>{question.description}</Questionnaire.Description>
96
<Questionnaire.Choices>
97
<For each={question.choices}>
98
{(choice) => (
99
<Questionnaire.Choice value={choice.value}>
100
<span class="font-medium">{choice.label}</span>
101
<Show when={"description" in choice ? choice.description : undefined}>
102
{(description) => (
103
<span class="text-muted-foreground">{description()}</span>
104
)}
105
</Show>
106
</Questionnaire.Choice>
107
)}
108
</For>
109
<Show when={"input" in question ? question.input : undefined}>
110
{(input) => (
111
<Questionnaire.Input
112
aria-label={input().label}
113
placeholder={input().placeholder}
114
/>
115
)}
116
</Show>
117
</Questionnaire.Choices>
118
<Questionnaire.Error />
119
</Questionnaire.Item>
120
)}
121
</For>
122
<Questionnaire.Actions>
123
<Questionnaire.Previous />
124
<Questionnaire.Skip />
125
<Questionnaire.Next>Next</Questionnaire.Next>
126
<Questionnaire.Submit>Save plan</Questionnaire.Submit>
127
</Questionnaire.Actions>
128
</Questionnaire.Root>
129
</>
130
);
131
}

Installation

CLI

pnpm dlx shadcn@latest add @zaidan/questionnaire
npx shadcn@latest add @zaidan/questionnaire
yarn dlx shadcn@latest add @zaidan/questionnaire
bunx --bun shadcn@latest add @zaidan/questionnaire

Manual

Install the following dependency:

pnpm add lucide-solid
npm i lucide-solid
yarn add lucide-solid
bun add lucide-solid

Copy the questionnaire folder into your project.

1
import {
2
QuestionnaireActions,
3
QuestionnaireChoice,
4
QuestionnaireChoiceDescription,
5
QuestionnaireChoiceInput,
6
QuestionnaireChoiceLabel,
7
QuestionnaireChoiceShortcut,
8
QuestionnaireChoices,
9
QuestionnaireDescription,
10
QuestionnaireError,
11
QuestionnaireInput,
12
QuestionnaireItem,
13
QuestionnaireNext,
14
QuestionnairePrevious,
15
QuestionnaireProgress,
16
QuestionnaireRoot,
17
QuestionnaireSkip,
18
QuestionnaireSubmit,
19
QuestionnaireTitle,
20
} from "./components";
21
import { useQuestionnaire } from "./context";
22
23
const Questionnaire = {
24
Root: QuestionnaireRoot,
25
Progress: QuestionnaireProgress,
26
Item: QuestionnaireItem,
27
Title: QuestionnaireTitle,
28
Description: QuestionnaireDescription,
29
Choices: QuestionnaireChoices,
30
Choice: QuestionnaireChoice,
31
ChoiceDescription: QuestionnaireChoiceDescription,
32
Input: QuestionnaireInput,
33
Error: QuestionnaireError,
34
Actions: QuestionnaireActions,
35
Previous: QuestionnairePrevious,
36
Skip: QuestionnaireSkip,
37
Next: QuestionnaireNext,
38
Submit: QuestionnaireSubmit,
39
// Headless choice sub-parts for custom compositions.
40
ChoiceInput: QuestionnaireChoiceInput,
41
ChoiceLabel: QuestionnaireChoiceLabel,
42
ChoiceShortcut: QuestionnaireChoiceShortcut,
43
};
44
45
export type {
46
QuestionnaireChoiceDefinition,
47
QuestionnaireInputType,
48
QuestionnaireItemDefinition,
49
QuestionnaireItemStatus,
50
QuestionnaireRootState,
51
QuestionnaireShortcutMode,
52
} from "./types";
53
54
export { Questionnaire, useQuestionnaire };

Usage

1
import { Questionnaire } from "~/components/blocks/questionnaire";
1
const items = [
2
{
3
name: "direction",
4
required: true,
5
prompt: "What should we prototype next?",
6
description: "Choose a direction or write your own.",
7
choices: [
8
{
9
value: "delegation",
10
label: "Delegation",
11
description: "Show how work moves to a specialist.",
12
},
13
{
14
value: "questions",
15
label: "Question prompts",
16
description: "Show choices while the interface waits.",
17
},
18
{ value: "both", label: "Both together" },
19
],
20
input: { label: "Another answer", placeholder: "Type another answer…" },
21
},
22
{
23
name: "detail",
24
required: false,
25
prompt: "How much detail should it include?",
26
description: "Skip this if you are not sure yet.",
27
choices: [
28
{ value: "focused", label: "Focused" },
29
{ value: "complete", label: "Complete flow" },
30
],
31
},
32
] as const;

Define the collection once: pass it to Questionnaire.Root for progress, actions, and shortcuts, then map it into the parts.

1
<Questionnaire.Root items={items} onSubmit={handleSubmit}>
2
<Questionnaire.Progress />
3
<For each={items}>
4
{(question) => (
5
<Questionnaire.Item name={question.name} required={question.required}>
6
<Questionnaire.Title>{question.prompt}</Questionnaire.Title>
7
<Questionnaire.Description>{question.description}</Questionnaire.Description>
8
<Questionnaire.Choices>
9
<For each={question.choices}>
10
{(choice) => (
11
<Questionnaire.Choice value={choice.value}>
12
<span class="font-medium">{choice.label}</span>
13
<Show when={"description" in choice ? choice.description : undefined}>
14
<span class="text-muted-foreground">{choice.description}</span>
15
</Show>
16
</Questionnaire.Choice>
17
)}
18
</For>
19
<Show when={"input" in question ? question.input : undefined}>
20
{(input) => (
21
<Questionnaire.Input aria-label={input().label} placeholder={input().placeholder} />
22
)}
23
</Show>
24
</Questionnaire.Choices>
25
<Questionnaire.Error />
26
</Questionnaire.Item>
27
)}
28
</For>
29
<Questionnaire.Actions>
30
<Questionnaire.Previous />
31
<Questionnaire.Skip />
32
<Questionnaire.Next />
33
<Questionnaire.Submit />
34
</Questionnaire.Actions>
35
</Questionnaire.Root>

The answers submit as regular form data:

1
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
2
event.preventDefault();
3
const answers = new FormData(event.currentTarget);
4
// answers.get("direction"), answers.getAll(...) for multiple items.
5
}

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.Submit

Questionnaire.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.

What context should the agent inspect?

Select every source that may affect the implementation.

Choose an answer to continue.

1
import { toast } from "solid-sonner";
2
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Toaster } from "~/components/ui/toast";
4
5
const items = [
6
{
7
choices: [{ value: "source" }, { value: "tests" }, { value: "docs" }, { value: "history" }],
8
name: "context",
9
required: true,
10
},
11
] as const;
12
13
export default function QuestionnaireMultiple() {
14
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
15
event.preventDefault();
16
17
const context = new FormData(event.currentTarget).getAll("context");
18
19
toast("Context selected", {
20
description: `Context: ${context.join(", ") || "None"}`,
21
});
22
}
23
24
return (
25
<>
26
<Toaster />
27
<Questionnaire.Root
28
class="mx-auto max-w-md"
29
items={items}
30
shortcuts="letters"
31
onSubmit={handleSubmit}
32
>
33
<Questionnaire.Item name="context" multiple required>
34
<Questionnaire.Title>What context should the agent inspect?</Questionnaire.Title>
35
<Questionnaire.Description>
36
Select every source that may affect the implementation.
37
</Questionnaire.Description>
38
<Questionnaire.Choices>
39
<Questionnaire.Choice value="source">Relevant source files</Questionnaire.Choice>
40
<Questionnaire.Choice value="tests">Existing tests</Questionnaire.Choice>
41
<Questionnaire.Choice value="docs">Architecture documentation</Questionnaire.Choice>
42
<Questionnaire.Choice value="history">Recent commit history</Questionnaire.Choice>
43
</Questionnaire.Choices>
44
<Questionnaire.Error />
45
</Questionnaire.Item>
46
47
<Questionnaire.Actions>
48
<Questionnaire.Submit>Share context</Questionnaire.Submit>
49
</Questionnaire.Actions>
50
</Questionnaire.Root>
51
</>
52
);
53
}

Freeform Answer

Compose Questionnaire.Input with fixed choices when the user can provide another answer.

How should the agent approach this refactor?

Choose a strategy or write a more specific instruction.

Choose an answer to continue.

1
import { toast } from "solid-sonner";
2
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Toaster } from "~/components/ui/toast";
4
5
const items = [
6
{
7
choices: [{ value: "incremental" }, { value: "module" }, { value: "rewrite" }],
8
name: "approach",
9
required: true,
10
},
11
] as const;
12
13
export default function QuestionnaireFreeform() {
14
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
15
event.preventDefault();
16
17
const approach = new FormData(event.currentTarget).get("approach");
18
19
toast("Approach selected", {
20
description: `Approach: ${approach ?? "None"}`,
21
});
22
}
23
24
return (
25
<>
26
<Toaster />
27
<Questionnaire.Root
28
class="mx-auto max-w-md"
29
items={items}
30
shortcuts="letters"
31
onSubmit={handleSubmit}
32
>
33
<Questionnaire.Item name="approach" required>
34
<Questionnaire.Title>How should the agent approach this refactor?</Questionnaire.Title>
35
<Questionnaire.Description>
36
Choose a strategy or write a more specific instruction.
37
</Questionnaire.Description>
38
<Questionnaire.Choices>
39
<Questionnaire.Choice value="incremental">
40
Make the smallest safe change
41
</Questionnaire.Choice>
42
<Questionnaire.Choice value="module">
43
Refactor one module at a time
44
</Questionnaire.Choice>
45
<Questionnaire.Choice value="rewrite">
46
Replace the implementation completely
47
</Questionnaire.Choice>
48
<Questionnaire.Input
49
aria-label="Another refactoring approach"
50
placeholder="Describe another approach…"
51
/>
52
</Questionnaire.Choices>
53
<Questionnaire.Error />
54
</Questionnaire.Item>
55
56
<Questionnaire.Actions>
57
<Questionnaire.Submit>Use this approach</Questionnaire.Submit>
58
</Questionnaire.Actions>
59
</Questionnaire.Root>
60
</>
61
);
62
}

Explicit Skip

Add Questionnaire.Skip when an optional item may be intentionally left unanswered.

Question 1 of 3
What kind of change is this?

Choose the category that best describes the work.

Choose an answer to continue.

Are there any implementation constraints?

Answer if needed, or intentionally skip this question.

How should the work be reviewed?

Choose the checks the agent should complete before handoff.

Choose an answer to continue.

1
import { createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import {
4
Questionnaire,
5
type QuestionnaireItemStatus,
6
} from "@/registry/kobalte/blocks/questionnaire";
7
import { Toaster } from "~/components/ui/toast";
8
9
const items = [
10
{ name: "task", required: true },
11
{ name: "constraints" },
12
{ name: "review", required: true },
13
] as const;
14
15
export default function QuestionnaireSkipDemo() {
16
const [constraintStatus, setConstraintStatus] =
17
createSignal<QuestionnaireItemStatus>("unanswered");
18
19
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
20
event.preventDefault();
21
22
const formData = new FormData(event.currentTarget);
23
const answers = {
24
task: formData.get("task"),
25
constraints: formData.get("constraints"),
26
constraintStatus: constraintStatus(),
27
review: formData.get("review"),
28
};
29
30
toast("Agent brief submitted", {
31
description: `Task: ${answers.task ?? "None"} · Constraints: ${
32
answers.constraintStatus === "skipped" ? "Skipped" : (answers.constraints ?? "None")
33
} · Review: ${answers.review ?? "None"}`,
34
});
35
}
36
37
return (
38
<>
39
<Toaster />
40
<Questionnaire.Root
41
class="mx-auto max-w-md"
42
defaultItem="task"
43
items={items}
44
onSubmit={handleSubmit}
45
>
46
<Questionnaire.Progress />
47
48
<Questionnaire.Item name="task" required>
49
<Questionnaire.Title>What kind of change is this?</Questionnaire.Title>
50
<Questionnaire.Description>
51
Choose the category that best describes the work.
52
</Questionnaire.Description>
53
<Questionnaire.Choices>
54
<Questionnaire.Choice value="feature">New feature</Questionnaire.Choice>
55
<Questionnaire.Choice value="fix">Bug fix</Questionnaire.Choice>
56
<Questionnaire.Choice value="refactor">Refactor</Questionnaire.Choice>
57
</Questionnaire.Choices>
58
<Questionnaire.Error />
59
</Questionnaire.Item>
60
61
<Questionnaire.Item name="constraints" onStatusChange={setConstraintStatus}>
62
<Questionnaire.Title>Are there any implementation constraints?</Questionnaire.Title>
63
<Questionnaire.Description>
64
Answer if needed, or intentionally skip this question.
65
</Questionnaire.Description>
66
<Questionnaire.Choices>
67
<Questionnaire.Choice value="no-dependencies">
68
Do not add dependencies
69
</Questionnaire.Choice>
70
<Questionnaire.Choice value="no-migrations">
71
Do not change the database
72
</Questionnaire.Choice>
73
<Questionnaire.Choice value="preserve-api">
74
Preserve the public API
75
</Questionnaire.Choice>
76
<Questionnaire.Input
77
aria-label="Another implementation constraint"
78
placeholder="Describe another constraint…"
79
/>
80
</Questionnaire.Choices>
81
</Questionnaire.Item>
82
83
<Questionnaire.Item name="review" required>
84
<Questionnaire.Title>How should the work be reviewed?</Questionnaire.Title>
85
<Questionnaire.Description>
86
Choose the checks the agent should complete before handoff.
87
</Questionnaire.Description>
88
<Questionnaire.Choices>
89
<Questionnaire.Choice value="tests">Run the test suite</Questionnaire.Choice>
90
<Questionnaire.Choice value="diff">Review the final diff</Questionnaire.Choice>
91
<Questionnaire.Choice value="both">Tests and diff review</Questionnaire.Choice>
92
</Questionnaire.Choices>
93
<Questionnaire.Error />
94
</Questionnaire.Item>
95
96
<Questionnaire.Actions>
97
<Questionnaire.Previous />
98
<Questionnaire.Skip />
99
<Questionnaire.Next>Next</Questionnaire.Next>
100
<Questionnaire.Submit>Submit brief</Questionnaire.Submit>
101
</Questionnaire.Actions>
102
</Questionnaire.Root>
103
</>
104
);
105
}

Shortcuts

Assign a letter or number key to each answer with shortcuts.

What should the agent do next?

Use the displayed shortcut or navigate with the keyboard.

Choose an answer to continue.

1
import { createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import {
4
Questionnaire,
5
type QuestionnaireShortcutMode,
6
} from "@/registry/kobalte/blocks/questionnaire";
7
import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select";
8
import { Toaster } from "~/components/ui/toast";
9
10
const items = [
11
{
12
choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }],
13
name: "action",
14
required: true,
15
},
16
] as const;
17
18
export default function QuestionnaireShortcuts() {
19
const [shortcuts, setShortcuts] = createSignal<QuestionnaireShortcutMode | undefined>("letters");
20
21
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
22
event.preventDefault();
23
24
const action = new FormData(event.currentTarget).get("action");
25
26
toast("Next action selected", {
27
description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts() ?? "none"}`,
28
});
29
}
30
31
return (
32
<div class="relative mx-auto flex h-full w-full max-w-md flex-col">
33
<Toaster />
34
<NativeSelect
35
aria-label="Shortcut style"
36
class="absolute end-0 top-0"
37
value={shortcuts() ?? "none"}
38
onChange={(event) => {
39
const value = event.currentTarget.value;
40
setShortcuts(value === "letters" || value === "numbers" ? value : undefined);
41
}}
42
>
43
<NativeSelectOption value="none">No shortcuts</NativeSelectOption>
44
<NativeSelectOption value="letters">Letters</NativeSelectOption>
45
<NativeSelectOption value="numbers">Numbers</NativeSelectOption>
46
</NativeSelect>
47
48
<Questionnaire.Root
49
class="mt-auto"
50
items={items}
51
shortcuts={shortcuts()}
52
onSubmit={handleSubmit}
53
>
54
<Questionnaire.Item name="action" required>
55
<Questionnaire.Title>What should the agent do next?</Questionnaire.Title>
56
<Questionnaire.Description>
57
Use the displayed shortcut or navigate with the keyboard.
58
</Questionnaire.Description>
59
<Questionnaire.Choices>
60
<Questionnaire.Choice value="inspect">Inspect the implementation</Questionnaire.Choice>
61
<Questionnaire.Choice value="tests">Run the relevant tests</Questionnaire.Choice>
62
<Questionnaire.Choice value="patch">Prepare the patch</Questionnaire.Choice>
63
</Questionnaire.Choices>
64
<Questionnaire.Error />
65
</Questionnaire.Item>
66
67
<Questionnaire.Actions>
68
<Questionnaire.Submit>Confirm action</Questionnaire.Submit>
69
</Questionnaire.Actions>
70
</Questionnaire.Root>
71
</div>
72
);
73
}

Custom Validation

Combine controlled navigation with an external schema such as Zod to return to an invalid item and present its error.

How much detail should the answer include?

Choose the response depth.

1 / 2

Choose an answer to continue.

Who will read the answer?

Public answers require complete context.

1 / 2

Choose an answer to continue.

1
import { createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { z } from "zod";
4
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
5
import { Card, CardAction, CardContent, CardFooter, CardHeader } from "~/components/ui/card";
6
import { Toaster } from "~/components/ui/toast";
7
8
const items = [
9
{ name: "detail", required: true },
10
{ name: "audience", required: true },
11
] as const;
12
13
const questionnaireSchema = z
14
.object({
15
detail: z.enum(["summary", "complete"]),
16
audience: z.enum(["team", "public"]),
17
})
18
.superRefine((answers, context) => {
19
if (answers.audience === "public" && answers.detail === "summary") {
20
context.addIssue({
21
code: "custom",
22
message: "Public answers need enough context. Choose a complete answer.",
23
path: ["detail"],
24
});
25
}
26
});
27
28
type QuestionnaireItemName = keyof z.infer<typeof questionnaireSchema>;
29
type QuestionnaireErrors = Partial<Record<QuestionnaireItemName, string>>;
30
31
function ValidationProgress() {
32
return (
33
<Questionnaire.Progress class="min-w-0">
34
{(state) => (
35
<>
36
{state.current} / {state.total}
37
</>
38
)}
39
</Questionnaire.Progress>
40
);
41
}
42
43
export default function QuestionnaireValidation() {
44
const [item, setItem] = createSignal("detail");
45
const [errors, setErrors] = createSignal<QuestionnaireErrors>({});
46
47
function clearError(name: QuestionnaireItemName) {
48
setErrors((currentErrors) => {
49
if (!currentErrors[name]) {
50
return currentErrors;
51
}
52
53
const nextErrors = { ...currentErrors };
54
delete nextErrors[name];
55
return nextErrors;
56
});
57
}
58
59
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
60
event.preventDefault();
61
62
const result = questionnaireSchema.safeParse(
63
Object.fromEntries(new FormData(event.currentTarget)),
64
);
65
66
if (result.success) {
67
setErrors({});
68
toast("Agent response configured", {
69
description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
70
});
71
return;
72
}
73
74
const nextErrors: QuestionnaireErrors = {};
75
76
for (const issue of result.error.issues) {
77
const name = issue.path[0];
78
79
if ((name === "detail" || name === "audience") && !nextErrors[name]) {
80
nextErrors[name] = issue.message;
81
}
82
}
83
84
const firstInvalidItem = result.error.issues[0]?.path[0];
85
86
setErrors(nextErrors);
87
88
if (firstInvalidItem === "detail" || firstInvalidItem === "audience") {
89
setItem(firstInvalidItem);
90
}
91
}
92
93
return (
94
<>
95
<Toaster />
96
<Questionnaire.Root
97
class="mx-auto max-w-md"
98
item={item()}
99
items={items}
100
onItemChange={setItem}
101
onSubmit={handleSubmit}
102
>
103
<Card class="w-full">
104
<Questionnaire.Item invalid={Boolean(errors().detail)} name="detail" required>
105
<CardHeader>
106
<Questionnaire.Title>How much detail should the answer include?</Questionnaire.Title>
107
<Questionnaire.Description>Choose the response depth.</Questionnaire.Description>
108
<CardAction>
109
<ValidationProgress />
110
</CardAction>
111
</CardHeader>
112
<CardContent>
113
<Questionnaire.Choices>
114
<Questionnaire.Choice value="summary" onChange={() => clearError("detail")}>
115
Concise summary
116
</Questionnaire.Choice>
117
<Questionnaire.Choice value="complete" onChange={() => clearError("detail")}>
118
Complete answer
119
</Questionnaire.Choice>
120
</Questionnaire.Choices>
121
<Questionnaire.Error>{errors().detail}</Questionnaire.Error>
122
</CardContent>
123
</Questionnaire.Item>
124
125
<Questionnaire.Item invalid={Boolean(errors().audience)} name="audience" required>
126
<CardHeader>
127
<Questionnaire.Title>Who will read the answer?</Questionnaire.Title>
128
<Questionnaire.Description>
129
Public answers require complete context.
130
</Questionnaire.Description>
131
<CardAction>
132
<ValidationProgress />
133
</CardAction>
134
</CardHeader>
135
<CardContent>
136
<Questionnaire.Choices>
137
<Questionnaire.Choice value="team" onChange={() => clearError("audience")}>
138
My team
139
</Questionnaire.Choice>
140
<Questionnaire.Choice value="public" onChange={() => clearError("audience")}>
141
Public audience
142
</Questionnaire.Choice>
143
</Questionnaire.Choices>
144
<Questionnaire.Error>{errors().audience}</Questionnaire.Error>
145
</CardContent>
146
</Questionnaire.Item>
147
148
<CardFooter>
149
<Questionnaire.Actions>
150
<Questionnaire.Previous />
151
<Questionnaire.Next>Next</Questionnaire.Next>
152
<Questionnaire.Submit>Validate answers</Questionnaire.Submit>
153
</Questionnaire.Actions>
154
</CardFooter>
155
</Card>
156
</Questionnaire.Root>
157
</>
158
);
159
}

Controlled

Control the active item from host state, such as returning to an invalid step.

Current checkpoint: Change scope

Question 1 of 3
What may the agent change?

The host stores the active checkpoint while Questionnaire navigates.

Choose an answer to continue.

Which verification level should it use?

Choose an answer to continue.

What should the agent return when finished?

Choose an answer to continue.

1
import { createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
4
import { Toaster } from "~/components/ui/toast";
5
6
const items = [
7
{ name: "scope", required: true },
8
{ name: "checks", required: true },
9
{ name: "output", required: true },
10
] as const;
11
12
const itemLabels: Record<string, string> = {
13
scope: "Change scope",
14
checks: "Verification",
15
output: "Final output",
16
};
17
18
export default function QuestionnaireControlled() {
19
const [item, setItem] = createSignal("scope");
20
21
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
22
event.preventDefault();
23
24
const formData = new FormData(event.currentTarget);
25
26
toast("Agent workflow configured", {
27
description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("checks") ?? "None"} · Output: ${formData.get("output") ?? "None"}`,
28
});
29
}
30
31
return (
32
<div class="relative mx-auto flex h-full w-full max-w-md flex-col">
33
<Toaster />
34
<p class="absolute end-0 top-0 text-muted-foreground text-sm" role="status">
35
Current checkpoint: {itemLabels[item()]}
36
</p>
37
38
<Questionnaire.Root
39
class="mt-auto"
40
item={item()}
41
items={items}
42
onItemChange={setItem}
43
onSubmit={handleSubmit}
44
>
45
<Questionnaire.Progress />
46
47
<Questionnaire.Item name="scope" required>
48
<Questionnaire.Title>What may the agent change?</Questionnaire.Title>
49
<Questionnaire.Description>
50
The host stores the active checkpoint while Questionnaire navigates.
51
</Questionnaire.Description>
52
<Questionnaire.Choices>
53
<Questionnaire.Choice value="component">Only the target component</Questionnaire.Choice>
54
<Questionnaire.Choice value="tests">Component and related tests</Questionnaire.Choice>
55
<Questionnaire.Choice value="feature">The complete feature area</Questionnaire.Choice>
56
</Questionnaire.Choices>
57
<Questionnaire.Error />
58
</Questionnaire.Item>
59
60
<Questionnaire.Item name="checks" required>
61
<Questionnaire.Title>Which verification level should it use?</Questionnaire.Title>
62
<Questionnaire.Choices>
63
<Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice>
64
<Questionnaire.Choice value="package">Package tests and typecheck</Questionnaire.Choice>
65
<Questionnaire.Choice value="full">Full workspace verification</Questionnaire.Choice>
66
</Questionnaire.Choices>
67
<Questionnaire.Error />
68
</Questionnaire.Item>
69
70
<Questionnaire.Item name="output" required>
71
<Questionnaire.Title>What should the agent return when finished?</Questionnaire.Title>
72
<Questionnaire.Choices>
73
<Questionnaire.Choice value="summary">Concise summary</Questionnaire.Choice>
74
<Questionnaire.Choice value="diff">Summary with changed files</Questionnaire.Choice>
75
<Questionnaire.Choice value="handoff">
76
Detailed implementation handoff
77
</Questionnaire.Choice>
78
</Questionnaire.Choices>
79
<Questionnaire.Error />
80
</Questionnaire.Item>
81
82
<Questionnaire.Actions>
83
<Questionnaire.Previous />
84
<Questionnaire.Next>Next</Questionnaire.Next>
85
<Questionnaire.Submit>Save workflow</Questionnaire.Submit>
86
</Questionnaire.Actions>
87
</Questionnaire.Root>
88
</div>
89
);
90
}

Resume

Restore a saved active item and default answers, then reset changes back to that saved state.

Question 2 of 3
What kind of migration is this?

This answer was saved during the previous session.

Choose an answer to continue.

How should the migration be verified?

These checks were selected during the previous session.

Choose an answer to continue.

Anything else the agent should remember?

This note was saved with the draft.

1
import { toast } from "solid-sonner";
2
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Button } from "~/components/ui/button";
4
import { Toaster } from "~/components/ui/toast";
5
6
const items = [
7
{ name: "change", required: true },
8
{ name: "verification", required: true },
9
{ name: "notes" },
10
] as const;
11
12
export default function QuestionnaireResume() {
13
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
14
event.preventDefault();
15
16
const formData = new FormData(event.currentTarget);
17
const answers = {
18
change: formData.get("change"),
19
verification: formData.getAll("verification"),
20
notes: formData.get("notes"),
21
};
22
23
toast("Draft updated", {
24
description: `Migration: ${answers.change ?? "None"} · Verification: ${answers.verification.join(", ") || "None"} · Notes: ${answers.notes || "None"}`,
25
});
26
}
27
28
return (
29
<>
30
<Toaster />
31
<Questionnaire.Root
32
class="mx-auto max-w-md"
33
defaultItem="verification"
34
items={items}
35
onReset={() => toast("Saved answers restored")}
36
onSubmit={handleSubmit}
37
>
38
<Questionnaire.Progress />
39
40
<Questionnaire.Item name="change" required>
41
<Questionnaire.Title>What kind of migration is this?</Questionnaire.Title>
42
<Questionnaire.Description>
43
This answer was saved during the previous session.
44
</Questionnaire.Description>
45
<Questionnaire.Choices>
46
<Questionnaire.Choice value="incremental" defaultChecked>
47
Incremental migration
48
</Questionnaire.Choice>
49
<Questionnaire.Choice value="cutover">Single cutover</Questionnaire.Choice>
50
</Questionnaire.Choices>
51
<Questionnaire.Error />
52
</Questionnaire.Item>
53
54
<Questionnaire.Item name="verification" multiple required>
55
<Questionnaire.Title>How should the migration be verified?</Questionnaire.Title>
56
<Questionnaire.Description>
57
These checks were selected during the previous session.
58
</Questionnaire.Description>
59
<Questionnaire.Choices>
60
<Questionnaire.Choice value="tests" defaultChecked>
61
Run migration tests
62
</Questionnaire.Choice>
63
<Questionnaire.Choice value="typecheck" defaultChecked>
64
Run the typecheck
65
</Questionnaire.Choice>
66
<Questionnaire.Choice value="manual">Perform a manual smoke test</Questionnaire.Choice>
67
</Questionnaire.Choices>
68
<Questionnaire.Error />
69
</Questionnaire.Item>
70
71
<Questionnaire.Item name="notes">
72
<Questionnaire.Title>Anything else the agent should remember?</Questionnaire.Title>
73
<Questionnaire.Description>This note was saved with the draft.</Questionnaire.Description>
74
<Questionnaire.Input
75
aria-label="Saved migration note"
76
defaultValue="Keep the existing public API stable."
77
/>
78
</Questionnaire.Item>
79
80
<Questionnaire.Actions>
81
<Button type="reset" variant="outline">
82
Reset changes
83
</Button>
84
<Questionnaire.Previous />
85
<Questionnaire.Next>Next</Questionnaire.Next>
86
<Questionnaire.Submit>Update draft</Questionnaire.Submit>
87
</Questionnaire.Actions>
88
</Questionnaire.Root>
89
</>
90
);
91
}

Conditional Items

Disable items that do not apply to the user's earlier answers.

Question 1 of 2
Where should the agent run?

Cloud runs add an environment question to this flow.

Choose an answer to continue.

Which cloud environment should it use?

Choose an answer to continue.

When should the agent request approval?

Choose an answer to continue.

1
import { createMemo, createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
4
import { Toaster } from "~/components/ui/toast";
5
6
export default function QuestionnaireConditional() {
7
const [runtime, setRuntime] = createSignal("local");
8
const items = createMemo(
9
() =>
10
[
11
{ name: "runtime", required: true },
12
{
13
disabled: runtime() !== "cloud",
14
name: "environment",
15
required: true,
16
},
17
{ name: "approval", required: true },
18
] as const,
19
);
20
21
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
22
event.preventDefault();
23
24
const formData = new FormData(event.currentTarget);
25
26
toast("Execution plan saved", {
27
description: `Runtime: ${formData.get("runtime") ?? "None"} · Environment: ${formData.get("environment") ?? "Not applicable"} · Approval: ${formData.get("approval") ?? "None"}`,
28
});
29
}
30
31
return (
32
<>
33
<Toaster />
34
<Questionnaire.Root
35
class="mx-auto max-w-md"
36
defaultItem="runtime"
37
items={items()}
38
onSubmit={handleSubmit}
39
>
40
<Questionnaire.Progress />
41
42
<Questionnaire.Item name="runtime" required>
43
<Questionnaire.Title>Where should the agent run?</Questionnaire.Title>
44
<Questionnaire.Description>
45
Cloud runs add an environment question to this flow.
46
</Questionnaire.Description>
47
<Questionnaire.Choices>
48
<Questionnaire.Choice
49
checked={runtime() === "local"}
50
value="local"
51
onChange={() => setRuntime("local")}
52
>
53
Local workspace
54
</Questionnaire.Choice>
55
<Questionnaire.Choice
56
checked={runtime() === "cloud"}
57
value="cloud"
58
onChange={() => setRuntime("cloud")}
59
>
60
Cloud workspace
61
</Questionnaire.Choice>
62
</Questionnaire.Choices>
63
<Questionnaire.Error />
64
</Questionnaire.Item>
65
66
<Questionnaire.Item disabled={runtime() !== "cloud"} name="environment" required>
67
<Questionnaire.Title>Which cloud environment should it use?</Questionnaire.Title>
68
<Questionnaire.Choices>
69
<Questionnaire.Choice value="preview">Preview</Questionnaire.Choice>
70
<Questionnaire.Choice value="staging">Staging</Questionnaire.Choice>
71
<Questionnaire.Choice value="isolated">Isolated sandbox</Questionnaire.Choice>
72
</Questionnaire.Choices>
73
<Questionnaire.Error />
74
</Questionnaire.Item>
75
76
<Questionnaire.Item name="approval" required>
77
<Questionnaire.Title>When should the agent request approval?</Questionnaire.Title>
78
<Questionnaire.Choices>
79
<Questionnaire.Choice value="writes">Before writing files</Questionnaire.Choice>
80
<Questionnaire.Choice value="commands">Before running commands</Questionnaire.Choice>
81
<Questionnaire.Choice value="sensitive">
82
Only for sensitive actions
83
</Questionnaire.Choice>
84
</Questionnaire.Choices>
85
<Questionnaire.Error />
86
</Questionnaire.Item>
87
88
<Questionnaire.Actions>
89
<Questionnaire.Previous />
90
<Questionnaire.Next>Next</Questionnaire.Next>
91
<Questionnaire.Submit>Save execution plan</Questionnaire.Submit>
92
</Questionnaire.Actions>
93
</Questionnaire.Root>
94
</>
95
);
96
}

Navigation State

Read item status to opt into disabled navigation and custom action styling.

Question 1 of 2
What may the agent modify?

Next is disabled until useQuestionnaire() reports the active item as answered.

Choose an answer to continue.

What must pass before completion?

Choose an answer to continue.

1
import { toast } from "solid-sonner";
2
import { Questionnaire, useQuestionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Toaster } from "~/components/ui/toast";
4
5
const items = [
6
{ name: "permission", required: true },
7
{ name: "verification", required: true },
8
] as const;
9
10
function NavigationActions() {
11
const state = useQuestionnaire();
12
const unanswered = () => state.activeItemStatus === "unanswered";
13
14
return (
15
<Questionnaire.Actions>
16
<Questionnaire.Previous />
17
<Questionnaire.Next
18
class="data-[status=unanswered]:opacity-50"
19
disabled={unanswered()}
20
variant="secondary"
21
>
22
Next ({state.current} of {state.total})
23
</Questionnaire.Next>
24
<Questionnaire.Submit disabled={unanswered()}>Save permissions</Questionnaire.Submit>
25
</Questionnaire.Actions>
26
);
27
}
28
29
export default function QuestionnaireNavigationState() {
30
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
31
event.preventDefault();
32
33
const formData = new FormData(event.currentTarget);
34
35
toast("Permissions saved", {
36
description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`,
37
});
38
}
39
40
return (
41
<>
42
<Toaster />
43
<Questionnaire.Root
44
class="mx-auto max-w-md"
45
defaultItem="permission"
46
items={items}
47
onSubmit={handleSubmit}
48
>
49
<Questionnaire.Progress />
50
51
<Questionnaire.Item name="permission" required>
52
<Questionnaire.Title>What may the agent modify?</Questionnaire.Title>
53
<Questionnaire.Description>
54
Next is disabled until useQuestionnaire() reports the active item as answered.
55
</Questionnaire.Description>
56
<Questionnaire.Choices>
57
<Questionnaire.Choice value="files">Project files</Questionnaire.Choice>
58
<Questionnaire.Choice value="tests">Project files and tests</Questionnaire.Choice>
59
<Questionnaire.Choice value="config">
60
Files, tests, and configuration
61
</Questionnaire.Choice>
62
</Questionnaire.Choices>
63
<Questionnaire.Error />
64
</Questionnaire.Item>
65
66
<Questionnaire.Item name="verification" required>
67
<Questionnaire.Title>What must pass before completion?</Questionnaire.Title>
68
<Questionnaire.Choices>
69
<Questionnaire.Choice value="tests">Tests</Questionnaire.Choice>
70
<Questionnaire.Choice value="types">Tests and types</Questionnaire.Choice>
71
<Questionnaire.Choice value="all">Tests, types, and visual QA</Questionnaire.Choice>
72
</Questionnaire.Choices>
73
<Questionnaire.Error />
74
</Questionnaire.Item>
75
76
<NavigationActions />
77
</Questionnaire.Root>
78
</>
79
);
80
}

Custom Progress

Pass a function child to Questionnaire.Progress to build a custom progress indicator from the root state.

Checkpoint 1 of 4
How large is the change?

Choose an answer to continue.

How should commits be organized?

Choose an answer to continue.

Which tests should run?

Choose an answer to continue.

How should the work be delivered?

Choose an answer to continue.

1
import { Index } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
4
import { Toaster } from "~/components/ui/toast";
5
6
const items = [
7
{ name: "scope", required: true },
8
{ name: "strategy", required: true },
9
{ name: "tests", required: true },
10
{ name: "delivery", required: true },
11
] as const;
12
13
export default function QuestionnaireProgressDemo() {
14
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
15
event.preventDefault();
16
17
const formData = new FormData(event.currentTarget);
18
19
toast("Pull request plan ready", {
20
description: `Scope: ${formData.get("scope") ?? "None"} · Commits: ${formData.get("strategy") ?? "None"} · Tests: ${formData.get("tests") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
21
});
22
}
23
24
return (
25
<>
26
<Toaster />
27
<Questionnaire.Root
28
class="mx-auto max-w-md"
29
defaultItem="scope"
30
items={items}
31
onSubmit={handleSubmit}
32
>
33
<Questionnaire.Progress class="w-full">
34
{(state) => (
35
<>
36
<div class="mb-2 flex gap-1.5" aria-hidden="true">
37
<Index each={Array.from({ length: state.total })}>
38
{(_, index) => (
39
<span
40
class={
41
index < state.current
42
? "h-1.5 flex-1 rounded-full bg-primary"
43
: "h-1.5 flex-1 rounded-full bg-muted"
44
}
45
/>
46
)}
47
</Index>
48
</div>
49
<span>
50
Checkpoint {state.current} of {state.total}
51
</span>
52
</>
53
)}
54
</Questionnaire.Progress>
55
56
<Questionnaire.Item name="scope" required>
57
<Questionnaire.Title>How large is the change?</Questionnaire.Title>
58
<Questionnaire.Choices>
59
<Questionnaire.Choice value="small">Small patch</Questionnaire.Choice>
60
<Questionnaire.Choice value="medium">Feature-sized change</Questionnaire.Choice>
61
<Questionnaire.Choice value="large">Cross-package change</Questionnaire.Choice>
62
</Questionnaire.Choices>
63
<Questionnaire.Error />
64
</Questionnaire.Item>
65
66
<Questionnaire.Item name="strategy" required>
67
<Questionnaire.Title>How should commits be organized?</Questionnaire.Title>
68
<Questionnaire.Choices>
69
<Questionnaire.Choice value="single">Single commit</Questionnaire.Choice>
70
<Questionnaire.Choice value="logical">Logical commits</Questionnaire.Choice>
71
<Questionnaire.Choice value="squash">Squash before review</Questionnaire.Choice>
72
</Questionnaire.Choices>
73
<Questionnaire.Error />
74
</Questionnaire.Item>
75
76
<Questionnaire.Item name="tests" required>
77
<Questionnaire.Title>Which tests should run?</Questionnaire.Title>
78
<Questionnaire.Choices>
79
<Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice>
80
<Questionnaire.Choice value="package">Package suite</Questionnaire.Choice>
81
<Questionnaire.Choice value="workspace">Full workspace</Questionnaire.Choice>
82
</Questionnaire.Choices>
83
<Questionnaire.Error />
84
</Questionnaire.Item>
85
86
<Questionnaire.Item name="delivery" required>
87
<Questionnaire.Title>How should the work be delivered?</Questionnaire.Title>
88
<Questionnaire.Choices>
89
<Questionnaire.Choice value="patch">Patch only</Questionnaire.Choice>
90
<Questionnaire.Choice value="commit">Committed locally</Questionnaire.Choice>
91
<Questionnaire.Choice value="branch">Push a review branch</Questionnaire.Choice>
92
</Questionnaire.Choices>
93
<Questionnaire.Error />
94
</Questionnaire.Item>
95
96
<Questionnaire.Actions>
97
<Questionnaire.Previous />
98
<Questionnaire.Next>Next</Questionnaire.Next>
99
<Questionnaire.Submit>Finish plan</Questionnaire.Submit>
100
</Questionnaire.Actions>
101
</Questionnaire.Root>
102
</>
103
);
104
}

Animated Items

Animate the active item while keeping progress and navigation stationary.

Question 1 of 3
What should the agent do?

Choose the task for this run.

Choose an answer to continue.

How should the work be reviewed?

Select the verification depth.

Choose an answer to continue.

How should the result be delivered?

Choose the final handoff format.

Choose an answer to continue.

1
import { toast } from "solid-sonner";
2
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Toaster } from "~/components/ui/toast";
4
5
const items = [
6
{ name: "task", required: true },
7
{ name: "review", required: true },
8
{ name: "delivery", required: true },
9
] as const;
10
11
const itemClass =
12
"data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none";
13
14
export default function QuestionnaireAnimated() {
15
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
16
event.preventDefault();
17
18
const formData = new FormData(event.currentTarget);
19
20
toast("Agent workflow saved", {
21
description: `Task: ${formData.get("task") ?? "None"} · Review: ${formData.get("review") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
22
});
23
}
24
25
return (
26
<>
27
<Toaster />
28
<Questionnaire.Root
29
class="mx-auto max-w-md"
30
defaultItem="task"
31
items={items}
32
onSubmit={handleSubmit}
33
>
34
<Questionnaire.Progress />
35
36
<Questionnaire.Item class={itemClass} name="task" required>
37
<Questionnaire.Title>What should the agent do?</Questionnaire.Title>
38
<Questionnaire.Description>Choose the task for this run.</Questionnaire.Description>
39
<Questionnaire.Choices>
40
<Questionnaire.Choice value="implement">
41
Implement the requested change
42
</Questionnaire.Choice>
43
<Questionnaire.Choice value="debug">Debug the current behavior</Questionnaire.Choice>
44
<Questionnaire.Choice value="review">Review the implementation</Questionnaire.Choice>
45
</Questionnaire.Choices>
46
<Questionnaire.Error />
47
</Questionnaire.Item>
48
49
<Questionnaire.Item class={itemClass} name="review" required>
50
<Questionnaire.Title>How should the work be reviewed?</Questionnaire.Title>
51
<Questionnaire.Description>Select the verification depth.</Questionnaire.Description>
52
<Questionnaire.Choices>
53
<Questionnaire.Choice value="targeted">Targeted checks</Questionnaire.Choice>
54
<Questionnaire.Choice value="complete">Complete test suite</Questionnaire.Choice>
55
<Questionnaire.Choice value="manual">Tests and manual QA</Questionnaire.Choice>
56
</Questionnaire.Choices>
57
<Questionnaire.Error />
58
</Questionnaire.Item>
59
60
<Questionnaire.Item class={itemClass} name="delivery" required>
61
<Questionnaire.Title>How should the result be delivered?</Questionnaire.Title>
62
<Questionnaire.Description>Choose the final handoff format.</Questionnaire.Description>
63
<Questionnaire.Choices>
64
<Questionnaire.Choice value="summary">Concise summary</Questionnaire.Choice>
65
<Questionnaire.Choice value="diff">Summary and changed files</Questionnaire.Choice>
66
<Questionnaire.Choice value="handoff">Detailed review handoff</Questionnaire.Choice>
67
</Questionnaire.Choices>
68
<Questionnaire.Error />
69
</Questionnaire.Item>
70
71
<Questionnaire.Actions>
72
<Questionnaire.Previous />
73
<Questionnaire.Next>Next</Questionnaire.Next>
74
<Questionnaire.Submit>Save workflow</Questionnaire.Submit>
75
</Questionnaire.Actions>
76
</Questionnaire.Root>
77
</>
78
);
79
}

Card

Compose Questionnaire with Card slots while keeping the question title and description semantic.

What should the agent work on?

Choose the task that should be handled next.

Question 1 of 2

Choose an answer to continue.

What should the final handoff include?

Pick the level of detail needed for review.

Question 1 of 2

Choose an answer to continue.

1
import { toast } from "solid-sonner";
2
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
3
import { Card, CardAction, CardContent, CardFooter, CardHeader } from "~/components/ui/card";
4
import { Toaster } from "~/components/ui/toast";
5
6
const items = [
7
{
8
choices: [{ value: "fix" }, { value: "refactor" }, { value: "docs" }],
9
name: "task",
10
required: true,
11
},
12
{
13
choices: [{ value: "summary" }, { value: "files" }, { value: "review" }],
14
name: "output",
15
required: true,
16
},
17
] as const;
18
19
export default function QuestionnaireCard() {
20
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
21
event.preventDefault();
22
23
const formData = new FormData(event.currentTarget);
24
25
toast("Agent task created", {
26
description: `Task: ${formData.get("task") ?? "None"} · Handoff: ${formData.get("output") ?? "None"}`,
27
});
28
}
29
30
return (
31
<>
32
<Toaster />
33
<Questionnaire.Root
34
class="mx-auto max-w-md"
35
defaultItem="task"
36
items={items}
37
shortcuts="numbers"
38
onSubmit={handleSubmit}
39
>
40
<Card>
41
<Questionnaire.Item name="task" required>
42
<CardHeader>
43
<Questionnaire.Title class="z-card-title z-font-heading">
44
What should the agent work on?
45
</Questionnaire.Title>
46
<Questionnaire.Description class="z-card-description">
47
Choose the task that should be handled next.
48
</Questionnaire.Description>
49
<CardAction>
50
<Questionnaire.Progress />
51
</CardAction>
52
</CardHeader>
53
<CardContent>
54
<Questionnaire.Choices>
55
<Questionnaire.Choice value="fix">Fix the failing tests</Questionnaire.Choice>
56
<Questionnaire.Choice value="refactor">
57
Refactor the data layer
58
</Questionnaire.Choice>
59
<Questionnaire.Choice value="docs">
60
Update the integration guide
61
</Questionnaire.Choice>
62
</Questionnaire.Choices>
63
<Questionnaire.Error />
64
</CardContent>
65
</Questionnaire.Item>
66
67
<Questionnaire.Item name="output" required>
68
<CardHeader>
69
<Questionnaire.Title class="z-card-title z-font-heading">
70
What should the final handoff include?
71
</Questionnaire.Title>
72
<Questionnaire.Description class="z-card-description">
73
Pick the level of detail needed for review.
74
</Questionnaire.Description>
75
<CardAction>
76
<Questionnaire.Progress />
77
</CardAction>
78
</CardHeader>
79
<CardContent>
80
<Questionnaire.Choices>
81
<Questionnaire.Choice value="summary">Summary only</Questionnaire.Choice>
82
<Questionnaire.Choice value="files">Summary and changed files</Questionnaire.Choice>
83
<Questionnaire.Choice value="review">Full review handoff</Questionnaire.Choice>
84
</Questionnaire.Choices>
85
<Questionnaire.Error />
86
</CardContent>
87
</Questionnaire.Item>
88
89
<CardFooter>
90
<Questionnaire.Actions class="w-full">
91
<Questionnaire.Previous />
92
<Questionnaire.Next>Next</Questionnaire.Next>
93
<Questionnaire.Submit>Create task</Questionnaire.Submit>
94
</Questionnaire.Actions>
95
</CardFooter>
96
</Card>
97
</Questionnaire.Root>
98
</>
99
);
100
}

Dialog

Compose Questionnaire inside a Dialog while keeping cancellation and dismissal host-owned.

1
import { createSignal } from "solid-js";
2
import { toast } from "solid-sonner";
3
import { Questionnaire } from "@/registry/kobalte/blocks/questionnaire";
4
import { Button } from "~/components/ui/button";
5
import {
6
Dialog,
7
DialogClose,
8
DialogContent,
9
DialogFooter,
10
DialogHeader,
11
DialogTrigger,
12
} from "~/components/ui/dialog";
13
import { Toaster } from "~/components/ui/toast";
14
15
const items = [
16
{ name: "scope", required: true },
17
{ name: "tests", required: true },
18
] as const;
19
20
export default function QuestionnaireDialog() {
21
const [open, setOpen] = createSignal(false);
22
23
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
24
event.preventDefault();
25
26
const formData = new FormData(event.currentTarget);
27
28
setOpen(false);
29
toast("Clarification sent", {
30
description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("tests") ?? "None"}`,
31
});
32
}
33
34
return (
35
<>
36
<Toaster />
37
<Dialog open={open()} onOpenChange={setOpen}>
38
<DialogTrigger as={Button} variant="outline">
39
Open clarification
40
</DialogTrigger>
41
<DialogContent>
42
<Questionnaire.Root defaultItem="scope" items={items} onSubmit={handleSubmit}>
43
<Questionnaire.Item name="scope" required>
44
<DialogHeader>
45
<Questionnaire.Progress />
46
<Questionnaire.Title class="z-dialog-title z-font-heading">
47
Which files are in scope?
48
</Questionnaire.Title>
49
<Questionnaire.Description class="z-dialog-description">
50
Choose how broadly the agent can update the workspace.
51
</Questionnaire.Description>
52
</DialogHeader>
53
<Questionnaire.Choices>
54
<Questionnaire.Choice value="component">Component only</Questionnaire.Choice>
55
<Questionnaire.Choice value="feature">
56
Complete feature directory
57
</Questionnaire.Choice>
58
<Questionnaire.Choice value="workspace">
59
Any related workspace file
60
</Questionnaire.Choice>
61
</Questionnaire.Choices>
62
<Questionnaire.Error />
63
</Questionnaire.Item>
64
65
<Questionnaire.Item name="tests" required>
66
<DialogHeader>
67
<Questionnaire.Progress />
68
<Questionnaire.Title class="z-dialog-title z-font-heading">
69
How much verification is needed?
70
</Questionnaire.Title>
71
<Questionnaire.Description class="z-dialog-description">
72
Choose the checks the agent should run before handoff.
73
</Questionnaire.Description>
74
</DialogHeader>
75
<Questionnaire.Choices>
76
<Questionnaire.Choice value="targeted">Targeted tests</Questionnaire.Choice>
77
<Questionnaire.Choice value="package">Package tests</Questionnaire.Choice>
78
<Questionnaire.Choice value="full">
79
Full workspace verification
80
</Questionnaire.Choice>
81
</Questionnaire.Choices>
82
<Questionnaire.Error />
83
</Questionnaire.Item>
84
85
<DialogFooter>
86
<DialogClose as={Button} type="button" variant="outline">
87
Cancel
88
</DialogClose>
89
<Questionnaire.Actions>
90
<Questionnaire.Previous />
91
<Questionnaire.Next>Next</Questionnaire.Next>
92
<Questionnaire.Submit>Send answer</Questionnaire.Submit>
93
</Questionnaire.Actions>
94
</DialogFooter>
95
</Questionnaire.Root>
96
</DialogContent>
97
</Dialog>
98
</>
99
);
100
}

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 shortcuts enabled, 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.

On This Page

  • Installation
    • CLI
    • Manual
  • Usage
  • Composition
  • Item Definitions
  • Multiple Selection
  • Freeform Answer
  • Explicit Skip
  • Shortcuts
  • Custom Validation
  • Controlled
  • Resume
  • Conditional Items
  • Navigation State
  • Custom Progress
  • Animated Items
  • Card
  • Dialog
  • Keyboard
  • Accessibility
  • API Reference
Built by Kevin Abatan. The source code is available on GitHub.