Questionnaire

PreviousNext

A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions.

Question 1 of 3
What should the agent build next?

Choose a direction or describe another task.

<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
    └── QuestionnaireSubmit

Questionnaire 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 items for item order, conditional items, and stable shortcut assignment
  • Controlled navigation with v-model:item for 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.

What context should the agent inspect?

Select every source that may affect the implementation.

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

How should the agent approach this refactor?

Choose a strategy or write a more specific instruction.

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

Question 1 of 3
What kind of change is this?

Choose the category that best describes the work.

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

What should the agent do next?

Use the displayed shortcut or navigate with the keyboard.

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

How much detail should the answer include?

Choose the response depth.

1 / 2
<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

Question 1 of 3
What may the agent change?

The host stores the active checkpoint while Questionnaire navigates.

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

Question 2 of 3
How should the migration be verified?

These checks were selected during the previous session.

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

Question 1 of 2
Where should the agent run?

Cloud runs add an environment question to this flow.

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

Read item status to opt into disabled navigation and custom action styling. Listen to @update:status on the items you want to track.

Question 1 of 2
What may the agent modify?

Next is intentionally disabled until an answer is selected.

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

Checkpoint 1 of 4
How large is the change?
<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.

Question 1 of 3
What should the agent do?

Choose the task for this run.

<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>
What should the agent work on?
Choose the task that should be handled next.
Question 1 of 2
<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

KeyDescription
Arrow DownMoves focus to the next answer.
Arrow UpMoves focus to the previous answer.
Arrow RightMoves to the next question once the current one is answered.
Arrow LeftMoves to the previous question.
EnterConfirms the focused answer and moves on, or submits on the last question.
Meta+Enter / Ctrl+EnterConfirms the active question from anywhere in the form.
A – Z / 1 – 9Selects 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.

PropTypeDefaultDescription
itemsQuestionnaireItemDefinition[]-Declares item order, required, disabled, and the choices used for shortcuts.
itemstring-The active item. Use with v-model:item.
defaultItemstring-The item shown first. Ignored when item is provided.
shortcuts"letters" | "numbers"-Assigns a keyboard shortcut to every choice.
noValidatebooleantrueSet to false to run native constraint validation on answered items.
classHTMLAttributes["class"]-Additional classes to apply to the form.
EmitPayloadDescription
update:itemstringThe active item changed.
submitEventEvery item is valid. Call event.preventDefault() to handle it yourself.
resetEventThe 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.

PropTypeDefaultDescription
classHTMLAttributes["class"]-Additional classes to apply to the progress bar.
asAsTag | Component"div"The element or component to render as.
asChildbooleanfalseRender the child element instead.

QuestionnaireItem

A single question, rendered as a fieldset. Only the active item is visible.

PropTypeDefaultDescription
namestring-Required. The name the answer submits under.
requiredbooleanfalseRequires an answer before the questionnaire can continue.
multiplebooleanfalseRenders choices as checkboxes and keeps every selected answer.
disabledbooleanfalseRemoves the item from the flow without unmounting it.
invalidbooleanfalseMarks the item invalid from outside, for example after schema validation.
classHTMLAttributes["class"]-Additional classes to apply to the item.
EmitPayloadDescription
update:status"unanswered" | "answered" | "skipped"The item status changed.

QuestionnaireTitle

The question, rendered as a legend.

PropTypeDefaultDescription
classHTMLAttributes["class"]-Additional classes to apply to the title.
asAsTag | Component"legend"The element or component to render as.
asChildbooleanfalseRender the child element instead.

QuestionnaireDescription

Help text associated with the item through aria-describedby.

PropTypeDefaultDescription
idstring-Overrides the generated id.
asAsTag | Component"p"The element or component to render as.
asChildbooleanfalseRender the child element instead.
classHTMLAttributes["class"]-Additional classes to apply to the description.

QuestionnaireChoices

The answer list. Wraps choices and an optional freeform input.

PropTypeDefaultDescription
classHTMLAttributes["class"]-Additional classes to apply to the list.
asAsTag | Component"div"The element or component to render as.
asChildbooleanfalseRender the child element instead.

QuestionnaireChoice

A single answer, rendered as a radio or a checkbox depending on the item.

PropTypeDefaultDescription
valuestring-Required. The submitted value.
checkedboolean-Controlled checked state. Use with v-model:checked.
defaultCheckedbooleanfalseChecks the choice on mount and after a native form reset.
disabledbooleanfalseDisables the choice.
classHTMLAttributes["class"]-Additional classes to apply to the choice.
EmitPayloadDescription
update:checkedbooleanThe choice was checked or cleared.
changeEventThe native change event.

QuestionnaireChoiceDescription

Secondary text inside a choice.

PropTypeDefaultDescription
classHTMLAttributes["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.

PropTypeDefaultDescription
modelValuestring | number-Controlled value. Use with v-model.
defaultValuestring | number-Fills the answer on mount and after a native reset.
typeQuestionnaireInputType"text"The input type.
disabledbooleanfalseDisables the input.
classHTMLAttributes["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.

PropTypeDefaultDescription
idstring-Overrides the generated id.
asAsTag | Component"p"The element or component to render as.
asChildbooleanfalseRender the child element instead.
classHTMLAttributes["class"]-Additional classes to apply to the error.

QuestionnaireActions

The navigation row.

PropTypeDefaultDescription
classHTMLAttributes["class"]-Additional classes to apply to the row.
asAsTag | Component"div"The element or component to render as.
asChildbooleanfalseRender 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.

PropTypeDefaultDescription
variantButtonVariants["variant"]"outline" / "default"The button variant.
sizeButtonVariants["size"]"default"The button size.
disabledbooleanfalseDisables the button.
asAsTag | Component"button"The element or component to render as.
asChildbooleanfalseRender the child element instead.
classHTMLAttributes["class"]-Additional classes to apply to the button.