Components

Questionnaire

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

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. It handles one question at a time, with progress, keyboard navigation, and validation built in.

Source code

Click to see the source code for this component on GitHub. Feel free to copy it and adjust it for your own use.

Installation

Usage

<template>
  <UiQuestionnaire :items="items" @submit="handleSubmit">
    <UiQuestionnaireProgress />

    <UiQuestionnaireItem name="direction" required>
      <UiQuestionnaireTitle>What should the agent build next?</UiQuestionnaireTitle>
      <UiQuestionnaireDescription>Choose a direction.</UiQuestionnaireDescription>
      <UiQuestionnaireChoices>
        <UiQuestionnaireChoice value="tool-calls">Tool call timeline</UiQuestionnaireChoice>
        <UiQuestionnaireChoice value="approvals">Approval checkpoints</UiQuestionnaireChoice>
      </UiQuestionnaireChoices>
      <UiQuestionnaireError />
    </UiQuestionnaireItem>

    <UiQuestionnaireItem name="timing" required>
      <UiQuestionnaireTitle>When should work begin?</UiQuestionnaireTitle>
      <UiQuestionnaireChoices>
        <UiQuestionnaireChoice value="now">Start now</UiQuestionnaireChoice>
        <UiQuestionnaireChoice value="backlog">Add it to the backlog</UiQuestionnaireChoice>
      </UiQuestionnaireChoices>
      <UiQuestionnaireError />
    </UiQuestionnaireItem>

    <UiQuestionnaireActions>
      <UiQuestionnairePrevious />
      <UiQuestionnaireSkip />
      <UiQuestionnaireNext />
      <UiQuestionnaireSubmit />
    </UiQuestionnaireActions>
  </UiQuestionnaire>
</template>

<script lang="ts" setup>
  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>

Composition

Use the following composition to build a questionnaire:

UiQuestionnaire
├── UiQuestionnaireProgress
├── UiQuestionnaireItem
   ├── UiQuestionnaireTitle
   ├── UiQuestionnaireDescription
   ├── UiQuestionnaireChoices
   ├── UiQuestionnaireChoice
   └── UiQuestionnaireChoiceDescription
   └── UiQuestionnaireInput
   └── UiQuestionnaireError
└── UiQuestionnaireActions
    ├── UiQuestionnairePrevious
    ├── UiQuestionnaireSkip
    ├── UiQuestionnaireNext
    └── UiQuestionnaireSubmit

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.

Freeform Answer

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

Explicit Skip

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

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.

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

UiQuestionnaireError falls back to a built-in message, so only render your own message when you have one:

<UiQuestionnaireError>
  <template v-if="errors.detail">
    {{ errors.detail }}
  </template>
</UiQuestionnaireError>

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.

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.

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.

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.

Custom Progress

Use the progress slot state to build a custom progress indicator. UiQuestionnaireProgress exposes current, total, first, and last.

Checkpoint 1 of 4
How large is the change?

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.

Card

Compose Questionnaire with Card slots while keeping the question title and description semantic. Use as-child to render a part as another component:

<UiQuestionnaireTitle as-child>
  <UiCardTitle>What should the agent work on?</UiCardTitle>
</UiQuestionnaireTitle>

What should the agent work on?

Choose the task that should be handled next.
Question 1 of 2

UiQuestionnaireProgress, UiQuestionnaireTitle, UiQuestionnaireDescription, UiQuestionnaireChoices, UiQuestionnaireError, UiQuestionnaireActions, 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.

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.
AZ / 19Selects the matching choice when shortcuts is set.

Accessibility

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

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