- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Questionnaire
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
<script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{
choices: [
{ value: 'tool-calls' },
{ value: 'approvals' },
{ value: 'handoffs' },
],
name: 'direction',
required: true,
},
{
choices: [
{ value: 'progress' },
{ value: 'decisions' },
{ value: 'risks' },
{ value: 'next-step' },
],
name: 'signals',
},
{
choices: [{ value: 'now' }, { value: 'next-cycle' }, { value: 'backlog' }],
name: 'timing',
required: true,
},
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
const signals = formData.getAll('signals')
toast('Agent plan saved', {
description: `Direction: ${formData.get('direction') ?? 'None'} · Progress signals: ${signals.join(', ') || 'None'} · Timing: ${formData.get('timing') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="direction"
:items="items"
shortcuts="letters"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem name="direction" required>
<QuestionnaireTitle>
What should the agent build next?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose a direction or describe another task.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tool-calls">
<span class="font-medium">Tool call timeline</span>
<span class="text-muted-foreground">
Show what the agent ran and what came back.
</span>
</QuestionnaireChoice>
<QuestionnaireChoice value="approvals">
<span class="font-medium">Approval checkpoints</span>
<span class="text-muted-foreground">
Ask before sensitive or destructive actions.
</span>
</QuestionnaireChoice>
<QuestionnaireChoice value="handoffs">
<span class="font-medium">Sub-agent handoffs</span>
<span class="text-muted-foreground">
Make delegated work and results easier to follow.
</span>
</QuestionnaireChoice>
<QuestionnaireInput
aria-label="Another agent feature"
placeholder="Describe another feature…"
/>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="signals" multiple>
<QuestionnaireTitle>
What should every progress update include?
</QuestionnaireTitle>
<QuestionnaireDescription>
Select all that apply, or skip this question.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="progress">
Progress
</QuestionnaireChoice>
<QuestionnaireChoice value="decisions">
Decisions
</QuestionnaireChoice>
<QuestionnaireChoice value="risks">
Risks
</QuestionnaireChoice>
<QuestionnaireChoice value="next-step">
Next step
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="timing" required>
<QuestionnaireTitle>
When should work begin?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose when the agent should begin the work.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="now">
Start now
</QuestionnaireChoice>
<QuestionnaireChoice value="next-cycle">
Next development cycle
</QuestionnaireChoice>
<QuestionnaireChoice value="backlog">
Add it to the backlog
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Save plan
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Installation
pnpm dlx shadcn-vue@latest add questionnaire
Usage
<script setup lang="ts">
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'direction', required: true },
{ name: 'timing', required: true },
]
function handleSubmit(event: Event) {
event.preventDefault()
const answers = new FormData(event.target as HTMLFormElement)
console.log(Object.fromEntries(answers))
}
</script>
<template>
<Questionnaire :items="items" @submit="handleSubmit">
<QuestionnaireProgress />
<QuestionnaireItem name="direction" required>
<QuestionnaireTitle>What should the agent build next?</QuestionnaireTitle>
<QuestionnaireDescription>Choose a direction.</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tool-calls">Tool call timeline</QuestionnaireChoice>
<QuestionnaireChoice value="approvals">Approval checkpoints</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="timing" required>
<QuestionnaireTitle>When should work begin?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="now">Start now</QuestionnaireChoice>
<QuestionnaireChoice value="backlog">Add it to the backlog</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext />
<QuestionnaireSubmit />
</QuestionnaireActions>
</Questionnaire>
</template>Composition
Use the following composition to build a questionnaire:
Questionnaire
├── QuestionnaireProgress
├── QuestionnaireItem
│ ├── QuestionnaireTitle
│ ├── QuestionnaireDescription
│ ├── QuestionnaireChoices
│ │ ├── QuestionnaireChoice
│ │ │ └── QuestionnaireChoiceDescription
│ │ └── QuestionnaireInput
│ └── QuestionnaireError
└── QuestionnaireActions
├── QuestionnairePrevious
├── QuestionnaireSkip
├── QuestionnaireNext
└── QuestionnaireSubmitQuestionnaire renders a real <form> and every item renders a <fieldset> with a <legend>, so answers submit with FormData and no extra state is needed.
Server Rendering
Pass items to server-render the active item, progress, actions, and answer shortcuts. Without it the questionnaire only learns its order once the items have mounted on the client.
Features
- One question at a time, with progress, navigation, and validation handled for you
- Single-choice, multiple-choice, freeform, and intentionally skipped answers
- Keyboard shortcuts for choices, plus arrow key navigation between questions and answers
- Declarative
itemsfor item order, conditional items, and stable shortcut assignment - Controlled navigation with
v-model:itemfor custom validation flows - Native form reset restores the answers you marked as defaults
Examples
Multiple Selection
Use multiple for an item that accepts more than one fixed answer.
<script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{
choices: [
{ value: 'source' },
{ value: 'tests' },
{ value: 'docs' },
{ value: 'history' },
],
name: 'context',
required: true,
},
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const context = new FormData(event.target as HTMLFormElement).getAll('context')
toast('Context selected', {
description: `Context: ${context.join(', ') || 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
:items="items"
shortcuts="letters"
@submit="handleSubmit"
>
<QuestionnaireItem name="context" multiple required>
<QuestionnaireTitle>
What context should the agent inspect?
</QuestionnaireTitle>
<QuestionnaireDescription>
Select every source that may affect the implementation.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="source">
Relevant source files
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Existing tests
</QuestionnaireChoice>
<QuestionnaireChoice value="docs">
Architecture documentation
</QuestionnaireChoice>
<QuestionnaireChoice value="history">
Recent commit history
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>
Share context
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Freeform Answer
Compose QuestionnaireInput with fixed choices when the user can provide another answer.
<script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{
choices: [
{ value: 'incremental' },
{ value: 'module' },
{ value: 'rewrite' },
],
name: 'approach',
required: true,
},
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const approach = new FormData(event.target as HTMLFormElement).get('approach')
toast('Approach selected', {
description: `Approach: ${approach ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
:items="items"
shortcuts="letters"
@submit="handleSubmit"
>
<QuestionnaireItem name="approach" required>
<QuestionnaireTitle>
How should the agent approach this refactor?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose a strategy or write a more specific instruction.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="incremental">
Make the smallest safe change
</QuestionnaireChoice>
<QuestionnaireChoice value="module">
Refactor one module at a time
</QuestionnaireChoice>
<QuestionnaireChoice value="rewrite">
Replace the implementation completely
</QuestionnaireChoice>
<QuestionnaireInput
aria-label="Another refactoring approach"
placeholder="Describe another approach…"
/>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>
Use this approach
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Explicit Skip
Add QuestionnaireSkip when an optional item may be intentionally left unanswered.
<script setup lang="ts">
import type { QuestionnaireItemStatus } from '@/components/ui/questionnaire'
import { ref } from 'vue'
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'task', required: true },
{ name: 'constraints' },
{ name: 'review', required: true },
] as const
const constraintStatus = ref<QuestionnaireItemStatus>('unanswered')
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
const constraints = constraintStatus.value === 'skipped'
? 'Skipped'
: (formData.get('constraints') ?? 'None')
toast('Agent brief submitted', {
description: `Task: ${formData.get('task') ?? 'None'} · Constraints: ${constraints} · Review: ${formData.get('review') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="task"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem name="task" required>
<QuestionnaireTitle>
What kind of change is this?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the category that best describes the work.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="feature">
New feature
</QuestionnaireChoice>
<QuestionnaireChoice value="fix">
Bug fix
</QuestionnaireChoice>
<QuestionnaireChoice value="refactor">
Refactor
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="constraints" @update:status="constraintStatus = $event">
<QuestionnaireTitle>
Are there any implementation constraints?
</QuestionnaireTitle>
<QuestionnaireDescription>
Answer if needed, or intentionally skip this question.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="no-dependencies">
Do not add dependencies
</QuestionnaireChoice>
<QuestionnaireChoice value="no-migrations">
Do not change the database
</QuestionnaireChoice>
<QuestionnaireChoice value="preserve-api">
Preserve the public API
</QuestionnaireChoice>
<QuestionnaireInput
aria-label="Another implementation constraint"
placeholder="Describe another constraint…"
/>
</QuestionnaireChoices>
</QuestionnaireItem>
<QuestionnaireItem name="review" required>
<QuestionnaireTitle>
How should the work be reviewed?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the checks the agent should complete before handoff.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests">
Run the test suite
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Review the final diff
</QuestionnaireChoice>
<QuestionnaireChoice value="both">
Tests and diff review
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Submit brief
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Shortcuts
Assign a letter or number key to each answer with shortcuts. Declare choices on items so the keys stay stable regardless of the render order.
<script setup lang="ts">
import type { QuestionnaireShortcutMode } from '@/components/ui/questionnaire'
import { computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{
choices: [{ value: 'inspect' }, { value: 'tests' }, { value: 'patch' }],
name: 'action',
required: true,
},
] as const
const mode = ref<'letters' | 'numbers' | 'none'>('letters')
const shortcuts = computed<QuestionnaireShortcutMode | undefined>(() =>
mode.value === 'none' ? undefined : mode.value)
function handleSubmit(event: Event) {
event.preventDefault()
const action = new FormData(event.target as HTMLFormElement).get('action')
toast('Next action selected', {
description: `Action: ${action ?? 'None'} · Shortcuts: ${shortcuts.value ?? 'none'}`,
})
}
</script>
<template>
<div class="relative mx-auto flex h-full w-full max-w-md flex-col">
<NativeSelect v-model="mode" class="absolute end-0 top-0" aria-label="Shortcut style">
<NativeSelectOption value="none">
No shortcuts
</NativeSelectOption>
<NativeSelectOption value="letters">
Letters
</NativeSelectOption>
<NativeSelectOption value="numbers">
Numbers
</NativeSelectOption>
</NativeSelect>
<Questionnaire
class="mt-auto"
:items="items"
:shortcuts="shortcuts"
@submit="handleSubmit"
>
<QuestionnaireItem name="action" required>
<QuestionnaireTitle>
What should the agent do next?
</QuestionnaireTitle>
<QuestionnaireDescription>
Use the displayed shortcut or navigate with the keyboard.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="inspect">
Inspect the implementation
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Run the relevant tests
</QuestionnaireChoice>
<QuestionnaireChoice value="patch">
Prepare the patch
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnaireSubmit>
Confirm action
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</div>
</template>Custom Validation
Combine controlled navigation with an external schema such as Zod to return to an invalid item and present its error.
<script setup lang="ts">
import { ref } from 'vue'
import { toast } from 'vue-sonner'
import { z } from 'zod'
import {
Card,
CardAction,
CardContent,
CardFooter,
CardHeader,
} from '@/components/ui/card'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
type ItemName = 'detail' | 'audience'
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: z.ZodIssueCode.custom,
message: 'Public answers need enough context. Choose a complete answer.',
path: ['detail'],
})
}
})
const item = ref<string>('detail')
const errors = ref<Partial<Record<ItemName, string>>>({})
function clearError(name: ItemName) {
if (errors.value[name]) {
delete errors.value[name]
}
}
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
const result = questionnaireSchema.safeParse(Object.fromEntries(formData))
if (result.success) {
errors.value = {}
toast('Agent response configured', {
description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
})
return
}
const nextErrors: Partial<Record<ItemName, string>> = {}
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]
errors.value = nextErrors
if (firstInvalidItem === 'detail' || firstInvalidItem === 'audience') {
item.value = firstInvalidItem
}
}
</script>
<template>
<Questionnaire
v-model:item="item"
class="mx-auto max-w-md"
:items="items"
@submit="handleSubmit"
>
<Card class="w-full">
<QuestionnaireItem :invalid="Boolean(errors.detail)" name="detail" required>
<CardHeader>
<QuestionnaireTitle>
How much detail should the answer include?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the response depth.
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress v-slot="{ current, total }" class="min-w-0">
{{ current }} / {{ total }}
</QuestionnaireProgress>
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary" @change="clearError('detail')">
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice value="complete" @change="clearError('detail')">
Complete answer
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError>
<template v-if="errors.detail">
{{ errors.detail }}
</template>
</QuestionnaireError>
</CardContent>
</QuestionnaireItem>
<QuestionnaireItem :invalid="Boolean(errors.audience)" name="audience" required>
<CardHeader>
<QuestionnaireTitle>
Who will read the answer?
</QuestionnaireTitle>
<QuestionnaireDescription>
Public answers require complete context.
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress v-slot="{ current, total }" class="min-w-0">
{{ current }} / {{ total }}
</QuestionnaireProgress>
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="team" @change="clearError('audience')">
My team
</QuestionnaireChoice>
<QuestionnaireChoice value="public" @change="clearError('audience')">
Public audience
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError>
<template v-if="errors.audience">
{{ errors.audience }}
</template>
</QuestionnaireError>
</CardContent>
</QuestionnaireItem>
<CardFooter>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Validate answers
</QuestionnaireSubmit>
</QuestionnaireActions>
</CardFooter>
</Card>
</Questionnaire>
</template>QuestionnaireError falls back to a built-in message, so only render your own message when you have one:
<QuestionnaireError>
<template v-if="errors.detail">
{{ errors.detail }}
</template>
</QuestionnaireError>Controlled
Control the active item from host state, such as returning to an invalid step. Use v-model:item.
Current checkpoint: Change scope
<script setup lang="ts">
import { computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'scope', required: true },
{ name: 'checks', required: true },
{ name: 'output', required: true },
] as const
const itemLabels: Record<string, string> = {
scope: 'Change scope',
checks: 'Verification',
output: 'Final output',
}
const item = ref('scope')
const label = computed(() => itemLabels[item.value])
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
toast('Agent workflow configured', {
description: `Scope: ${formData.get('scope') ?? 'None'} · Verification: ${formData.get('checks') ?? 'None'} · Output: ${formData.get('output') ?? 'None'}`,
})
}
</script>
<template>
<div class="relative mx-auto flex h-full w-full max-w-md flex-col">
<p class="text-muted-foreground absolute end-0 top-0 text-sm" role="status">
Current checkpoint: {{ label }}
</p>
<Questionnaire
v-model:item="item"
class="mt-auto"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem name="scope" required>
<QuestionnaireTitle>
What may the agent change?
</QuestionnaireTitle>
<QuestionnaireDescription>
The host stores the active checkpoint while Questionnaire navigates.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="component">
Only the target component
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Component and related tests
</QuestionnaireChoice>
<QuestionnaireChoice value="feature">
The complete feature area
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="checks" required>
<QuestionnaireTitle>
Which verification level should it use?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package tests and typecheck
</QuestionnaireChoice>
<QuestionnaireChoice value="full">
Full workspace verification
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="output" required>
<QuestionnaireTitle>
What should the agent return when finished?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Summary with changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="handoff">
Detailed implementation handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Save workflow
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</div>
</template>Resume
Restore a saved active item and default answers, then reset changes back to that saved state.
<script setup lang="ts">
import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'change', required: true },
{ name: 'verification', required: true },
{ name: 'notes' },
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
const verification = formData.getAll('verification')
toast('Draft updated', {
description: `Migration: ${formData.get('change') ?? 'None'} · Verification: ${verification.join(', ') || 'None'} · Notes: ${formData.get('notes') || 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="verification"
:items="items"
@reset="toast('Saved answers restored')"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem name="change" required>
<QuestionnaireTitle>
What kind of migration is this?
</QuestionnaireTitle>
<QuestionnaireDescription>
This answer was saved during the previous session.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="incremental" default-checked>
Incremental migration
</QuestionnaireChoice>
<QuestionnaireChoice value="cutover">
Single cutover
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="verification" multiple required>
<QuestionnaireTitle>
How should the migration be verified?
</QuestionnaireTitle>
<QuestionnaireDescription>
These checks were selected during the previous session.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests" default-checked>
Run migration tests
</QuestionnaireChoice>
<QuestionnaireChoice value="typecheck" default-checked>
Run the typecheck
</QuestionnaireChoice>
<QuestionnaireChoice value="manual">
Perform a manual smoke test
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="notes">
<QuestionnaireTitle>
Anything else the agent should remember?
</QuestionnaireTitle>
<QuestionnaireDescription>
This note was saved with the draft.
</QuestionnaireDescription>
<QuestionnaireInput
aria-label="Saved migration note"
default-value="Keep the existing public API stable."
/>
</QuestionnaireItem>
<QuestionnaireActions>
<Button type="reset" variant="outline">
Reset changes
</Button>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Update draft
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Conditional Items
Disable items that do not apply to the user's earlier answers.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const runtime = ref('local')
const items = computed(() => [
{ name: 'runtime', required: true },
{ disabled: runtime.value !== 'cloud', name: 'environment', required: true },
{ name: 'approval', required: true },
])
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
toast('Execution plan saved', {
description: `Runtime: ${formData.get('runtime') ?? 'None'} · Environment: ${formData.get('environment') ?? 'Not applicable'} · Approval: ${formData.get('approval') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="runtime"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem name="runtime" required>
<QuestionnaireTitle>
Where should the agent run?
</QuestionnaireTitle>
<QuestionnaireDescription>
Cloud runs add an environment question to this flow.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice
:checked="runtime === 'local'"
value="local"
@change="runtime = 'local'"
>
Local workspace
</QuestionnaireChoice>
<QuestionnaireChoice
:checked="runtime === 'cloud'"
value="cloud"
@change="runtime = 'cloud'"
>
Cloud workspace
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem
name="environment"
:disabled="runtime !== 'cloud'"
required
>
<QuestionnaireTitle>
Which cloud environment should it use?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="preview">
Preview
</QuestionnaireChoice>
<QuestionnaireChoice value="staging">
Staging
</QuestionnaireChoice>
<QuestionnaireChoice value="isolated">
Isolated sandbox
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="approval" required>
<QuestionnaireTitle>
When should the agent request approval?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="writes">
Before writing files
</QuestionnaireChoice>
<QuestionnaireChoice value="commands">
Before running commands
</QuestionnaireChoice>
<QuestionnaireChoice value="sensitive">
Only for sensitive actions
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Save execution plan
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Navigation State
Read item status to opt into disabled navigation and custom action styling. Listen to @update:status on the items you want to track.
<script setup lang="ts">
import type { QuestionnaireItemStatus } from '@/components/ui/questionnaire'
import { computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
type ItemName = 'permission' | 'verification'
const items = [
{ name: 'permission', required: true },
{ name: 'verification', required: true },
] as const
const item = ref<ItemName>('permission')
const statuses = ref<Record<ItemName, QuestionnaireItemStatus>>({
permission: 'unanswered',
verification: 'unanswered',
})
const unanswered = computed(() => statuses.value[item.value] === 'unanswered')
function setStatus(name: ItemName, status: QuestionnaireItemStatus) {
statuses.value = { ...statuses.value, [name]: status }
}
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
toast('Permissions saved', {
description: `Permission: ${formData.get('permission') ?? 'None'} · Verification: ${formData.get('verification') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
v-model:item="item"
class="mx-auto max-w-md"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem
name="permission"
required
@update:status="setStatus('permission', $event)"
>
<QuestionnaireTitle>What may the agent modify?</QuestionnaireTitle>
<QuestionnaireDescription>
Next is intentionally disabled until an answer is selected.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="files">
Project files
</QuestionnaireChoice>
<QuestionnaireChoice value="tests">
Project files and tests
</QuestionnaireChoice>
<QuestionnaireChoice value="config">
Files, tests, and configuration
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem
name="verification"
required
@update:status="setStatus('verification', $event)"
>
<QuestionnaireTitle>
What must pass before completion?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="tests">
Tests
</QuestionnaireChoice>
<QuestionnaireChoice value="types">
Tests and types
</QuestionnaireChoice>
<QuestionnaireChoice value="all">
Tests, types, and visual QA
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext
class="data-[status=unanswered]:opacity-50"
:disabled="unanswered"
variant="secondary"
>
Next
</QuestionnaireNext>
<QuestionnaireSubmit :disabled="unanswered">
Save permissions
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Custom Progress
Use the progress slot state to build a custom progress indicator. QuestionnaireProgress exposes current, total, first, and last.
<script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'scope', required: true },
{ name: 'strategy', required: true },
{ name: 'tests', required: true },
{ name: 'delivery', required: true },
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
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'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="scope"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress v-slot="{ current, total }" class="w-full">
<div aria-hidden="true" class="mb-2 flex gap-1.5">
<span
v-for="step in total"
:key="step"
class="h-1.5 flex-1 rounded-full"
:class="step <= current ? 'bg-primary' : 'bg-muted'"
/>
</div>
<span>Checkpoint {{ current }} of {{ total }}</span>
</QuestionnaireProgress>
<QuestionnaireItem name="scope" required>
<QuestionnaireTitle>
How large is the change?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="small">
Small patch
</QuestionnaireChoice>
<QuestionnaireChoice value="medium">
Feature-sized change
</QuestionnaireChoice>
<QuestionnaireChoice value="large">
Cross-package change
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="strategy" required>
<QuestionnaireTitle>
How should commits be organized?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="single">
Single commit
</QuestionnaireChoice>
<QuestionnaireChoice value="logical">
Logical commits
</QuestionnaireChoice>
<QuestionnaireChoice value="squash">
Squash before review
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="tests" required>
<QuestionnaireTitle>Which tests should run?</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package suite
</QuestionnaireChoice>
<QuestionnaireChoice value="workspace">
Full workspace
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="delivery" required>
<QuestionnaireTitle>
How should the work be delivered?
</QuestionnaireTitle>
<QuestionnaireChoices>
<QuestionnaireChoice value="patch">
Patch only
</QuestionnaireChoice>
<QuestionnaireChoice value="commit">
Committed locally
</QuestionnaireChoice>
<QuestionnaireChoice value="branch">
Push a review branch
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Finish plan
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Animated Items
Animate the active item while keeping progress and navigation stationary. The active item is marked with data-active.
<script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'task', required: true },
{ name: 'review', required: true },
{ name: 'delivery', required: true },
] as const
const 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'
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
toast('Agent workflow saved', {
description: `Task: ${formData.get('task') ?? 'None'} · Review: ${formData.get('review') ?? 'None'} · Delivery: ${formData.get('delivery') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="task"
:items="items"
@submit="handleSubmit"
>
<QuestionnaireProgress />
<QuestionnaireItem :class="itemClass" name="task" required>
<QuestionnaireTitle>
What should the agent do?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the task for this run.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="implement">
Implement the requested change
</QuestionnaireChoice>
<QuestionnaireChoice value="debug">
Debug the current behavior
</QuestionnaireChoice>
<QuestionnaireChoice value="review">
Review the implementation
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem :class="itemClass" name="review" required>
<QuestionnaireTitle>
How should the work be reviewed?
</QuestionnaireTitle>
<QuestionnaireDescription>
Select the verification depth.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted checks
</QuestionnaireChoice>
<QuestionnaireChoice value="complete">
Complete test suite
</QuestionnaireChoice>
<QuestionnaireChoice value="manual">
Tests and manual QA
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem :class="itemClass" name="delivery" required>
<QuestionnaireTitle>
How should the result be delivered?
</QuestionnaireTitle>
<QuestionnaireDescription>
Choose the final handoff format.
</QuestionnaireDescription>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Concise summary
</QuestionnaireChoice>
<QuestionnaireChoice value="diff">
Summary and changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="handoff">
Detailed review handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Save workflow
</QuestionnaireSubmit>
</QuestionnaireActions>
</Questionnaire>
</template>Card
Compose Questionnaire with Card slots while keeping the question title and description semantic. Use as-child to render a part as another component:
<QuestionnaireTitle as-child>
<CardTitle>What should the agent work on?</CardTitle>
</QuestionnaireTitle><script setup lang="ts">
import { toast } from 'vue-sonner'
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{
choices: [{ value: 'fix' }, { value: 'refactor' }, { value: 'docs' }],
name: 'task',
required: true,
},
{
choices: [{ value: 'summary' }, { value: 'files' }, { value: 'review' }],
name: 'output',
required: true,
},
] as const
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
toast('Agent task created', {
description: `Task: ${formData.get('task') ?? 'None'} · Handoff: ${formData.get('output') ?? 'None'}`,
})
}
</script>
<template>
<Questionnaire
class="mx-auto max-w-md"
default-item="task"
:items="items"
shortcuts="numbers"
@submit="handleSubmit"
>
<Card>
<QuestionnaireItem name="task" required>
<CardHeader>
<QuestionnaireTitle as-child>
<CardTitle>What should the agent work on?</CardTitle>
</QuestionnaireTitle>
<QuestionnaireDescription as-child>
<CardDescription>
Choose the task that should be handled next.
</CardDescription>
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="fix">
Fix the failing tests
</QuestionnaireChoice>
<QuestionnaireChoice value="refactor">
Refactor the data layer
</QuestionnaireChoice>
<QuestionnaireChoice value="docs">
Update the integration guide
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</CardContent>
</QuestionnaireItem>
<QuestionnaireItem name="output" required>
<CardHeader>
<QuestionnaireTitle as-child>
<CardTitle>What should the final handoff include?</CardTitle>
</QuestionnaireTitle>
<QuestionnaireDescription as-child>
<CardDescription>
Pick the level of detail needed for review.
</CardDescription>
</QuestionnaireDescription>
<CardAction>
<QuestionnaireProgress />
</CardAction>
</CardHeader>
<CardContent>
<QuestionnaireChoices>
<QuestionnaireChoice value="summary">
Summary only
</QuestionnaireChoice>
<QuestionnaireChoice value="files">
Summary and changed files
</QuestionnaireChoice>
<QuestionnaireChoice value="review">
Full review handoff
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</CardContent>
</QuestionnaireItem>
<CardFooter>
<QuestionnaireActions class="w-full">
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Create task
</QuestionnaireSubmit>
</QuestionnaireActions>
</CardFooter>
</Card>
</Questionnaire>
</template>QuestionnaireProgress, QuestionnaireTitle, QuestionnaireDescription, QuestionnaireChoices, QuestionnaireError, QuestionnaireActions, and the four navigation buttons all accept as and as-child. as-child on the title replaces the legend that names the item, so the item labels itself with the rendered title instead. The title and description keep the id of the child they render.
Dialog
Compose Questionnaire inside a Dialog while keeping cancellation and dismissal host-owned.
<script setup lang="ts">
import { ref } from 'vue'
import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from '@/components/ui/questionnaire'
const items = [
{ name: 'scope', required: true },
{ name: 'tests', required: true },
] as const
const open = ref(false)
function handleSubmit(event: Event) {
event.preventDefault()
const formData = new FormData(event.target as HTMLFormElement)
open.value = false
toast('Clarification sent', {
description: `Scope: ${formData.get('scope') ?? 'None'} · Verification: ${formData.get('tests') ?? 'None'}`,
})
}
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<Button variant="outline">
Open clarification
</Button>
</DialogTrigger>
<DialogContent>
<Questionnaire default-item="scope" :items="items" @submit="handleSubmit">
<QuestionnaireItem name="scope" required>
<DialogHeader>
<QuestionnaireProgress />
<QuestionnaireTitle as-child>
<DialogTitle>Which files are in scope?</DialogTitle>
</QuestionnaireTitle>
<QuestionnaireDescription as-child>
<DialogDescription>
Choose how broadly the agent can update the workspace.
</DialogDescription>
</QuestionnaireDescription>
</DialogHeader>
<QuestionnaireChoices>
<QuestionnaireChoice value="component">
Component only
</QuestionnaireChoice>
<QuestionnaireChoice value="feature">
Complete feature directory
</QuestionnaireChoice>
<QuestionnaireChoice value="workspace">
Any related workspace file
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="tests" required>
<DialogHeader>
<QuestionnaireProgress />
<QuestionnaireTitle as-child>
<DialogTitle>How much verification is needed?</DialogTitle>
</QuestionnaireTitle>
<QuestionnaireDescription as-child>
<DialogDescription>
Choose the checks the agent should run before handoff.
</DialogDescription>
</QuestionnaireDescription>
</DialogHeader>
<QuestionnaireChoices>
<QuestionnaireChoice value="targeted">
Targeted tests
</QuestionnaireChoice>
<QuestionnaireChoice value="package">
Package tests
</QuestionnaireChoice>
<QuestionnaireChoice value="full">
Full workspace verification
</QuestionnaireChoice>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<DialogFooter>
<DialogClose as-child>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext>
Next
</QuestionnaireNext>
<QuestionnaireSubmit>
Send answer
</QuestionnaireSubmit>
</QuestionnaireActions>
</DialogFooter>
</Questionnaire>
</DialogContent>
</Dialog>
</template>Keyboard navigation
| Key | Description |
|---|---|
Arrow Down | Moves focus to the next answer. |
Arrow Up | Moves focus to the previous answer. |
Arrow Right | Moves to the next question once the current one is answered. |
Arrow Left | Moves to the previous question. |
Enter | Confirms the focused answer and moves on, or submits on the last question. |
Meta+Enter / Ctrl+Enter | Confirms the active question from anywhere in the form. |
A – Z / 1 – 9 | Selects the matching choice when shortcuts is set. |
Accessibility
QuestionnaireItem renders a fieldset with a legend, so every question is announced with its answers. Descriptions and errors are associated with the item through aria-describedby, and an invalid item exposes aria-invalid.
QuestionnaireProgress renders a named progressbar that announces the current question. Inactive items are hidden and inert, so they stay out of the tab order and the accessibility tree.
Navigation actions are real buttons. QuestionnaireSubmit submits the form, so a questionnaire keeps working with browser autofill and native form submission.
API Reference
Questionnaire
The root form. Manages the active item, progress, validation, and keyboard navigation.
| Prop | Type | Default | Description |
|---|---|---|---|
items | QuestionnaireItemDefinition[] | - | Declares item order, required, disabled, and the choices used for shortcuts. |
item | string | - | The active item. Use with v-model:item. |
defaultItem | string | - | The item shown first. Ignored when item is provided. |
shortcuts | "letters" | "numbers" | - | Assigns a keyboard shortcut to every choice. |
noValidate | boolean | true | Set to false to run native constraint validation on answered items. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the form. |
| Emit | Payload | Description |
|---|---|---|
update:item | string | The active item changed. |
submit | Event | Every item is valid. Call event.preventDefault() to handle it yourself. |
reset | Event | The form was reset. Call event.preventDefault() to keep the answers. |
QuestionnaireProgress
A progressbar announcing the active question. Exposes current, total, first, and last to its default slot.
| Prop | Type | Default | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the progress bar. |
as | AsTag | Component | "div" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
QuestionnaireItem
A single question, rendered as a fieldset. Only the active item is visible.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Required. The name the answer submits under. |
required | boolean | false | Requires an answer before the questionnaire can continue. |
multiple | boolean | false | Renders choices as checkboxes and keeps every selected answer. |
disabled | boolean | false | Removes the item from the flow without unmounting it. |
invalid | boolean | false | Marks the item invalid from outside, for example after schema validation. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the item. |
| Emit | Payload | Description |
|---|---|---|
update:status | "unanswered" | "answered" | "skipped" | The item status changed. |
QuestionnaireTitle
The question, rendered as a legend.
| Prop | Type | Default | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the title. |
as | AsTag | Component | "legend" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
QuestionnaireDescription
Help text associated with the item through aria-describedby.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | Overrides the generated id. |
as | AsTag | Component | "p" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the description. |
QuestionnaireChoices
The answer list. Wraps choices and an optional freeform input.
| Prop | Type | Default | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the list. |
as | AsTag | Component | "div" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
QuestionnaireChoice
A single answer, rendered as a radio or a checkbox depending on the item.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Required. The submitted value. |
checked | boolean | - | Controlled checked state. Use with v-model:checked. |
defaultChecked | boolean | false | Checks the choice on mount and after a native form reset. |
disabled | boolean | false | Disables the choice. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the choice. |
| Emit | Payload | Description |
|---|---|---|
update:checked | boolean | The choice was checked or cleared. |
change | Event | The native change event. |
QuestionnaireChoiceDescription
Secondary text inside a choice.
| Prop | Type | Default | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the description. |
QuestionnaireInput
A freeform answer. Answers the item while it holds a value, and submits under the item name.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | - | Controlled value. Use with v-model. |
defaultValue | string | number | - | Fills the answer on mount and after a native reset. |
type | QuestionnaireInputType | "text" | The input type. |
disabled | boolean | false | Disables the input. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the input. |
QuestionnaireError
The item error. Hidden until the item is invalid, and falls back to a built-in message.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | Overrides the generated id. |
as | AsTag | Component | "p" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the error. |
QuestionnaireActions
The navigation row.
| Prop | Type | Default | Description |
|---|---|---|---|
class | HTMLAttributes["class"] | - | Additional classes to apply to the row. |
as | AsTag | Component | "div" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
QuestionnairePrevious, QuestionnaireSkip, QuestionnaireNext, and QuestionnaireSubmit
Navigation buttons. Each one hides itself when it does not apply: QuestionnairePrevious on the first item, QuestionnaireSkip on required items, QuestionnaireNext on the last item, and QuestionnaireSubmit everywhere but the last item.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ButtonVariants["variant"] | "outline" / "default" | The button variant. |
size | ButtonVariants["size"] | "default" | The button size. |
disabled | boolean | false | Disables the button. |
as | AsTag | Component | "button" | The element or component to render as. |
asChild | boolean | false | Render the child element instead. |
class | HTMLAttributes["class"] | - | Additional classes to apply to the button. |
On This Page
InstallationUsageCompositionServer RenderingFeaturesExamplesMultiple SelectionFreeform AnswerExplicit SkipShortcutsCustom ValidationControlledResumeConditional ItemsNavigation StateCustom ProgressAnimated ItemsCardDialogKeyboard navigationAccessibilityAPI ReferenceQuestionnaireQuestionnaireProgressQuestionnaireItemQuestionnaireTitleQuestionnaireDescriptionQuestionnaireChoicesQuestionnaireChoiceQuestionnaireChoiceDescriptionQuestionnaireInputQuestionnaireErrorQuestionnaireActionsQuestionnairePrevious, QuestionnaireSkip, QuestionnaireNext, and QuestionnaireSubmit