Dots

Questionnaire

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.

"use client"

import * as React from "react"

Installation

pnpm dlx shadcn@latest add @dots/questionnaire

Usage

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoiceDescription,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const items = [
  {
    name: "direction",
    required: true,
    prompt: "What should we prototype next?",
    description: "Choose a direction or write your own.",
    choices: [
      {
        value: "delegation",
        label: "Delegation",
        description: "Show how work moves to a specialist.",
      },
      {
        value: "questions",
        label: "Question prompts",
        description: "Show choices while the interface waits.",
      },
      { value: "both", label: "Both together" },
    ],
    input: { label: "Another answer", placeholder: "Type another answerโ€ฆ" },
  },
  {
    name: "detail",
    required: false,
    prompt: "How much detail should it include?",
    description: "Skip this if you are not sure yet.",
    choices: [
      { value: "focused", label: "Focused" },
      { value: "complete", label: "Complete flow" },
    ],
  },
] as const

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

<Questionnaire items={items} onSubmit={handleSubmit}>
  <QuestionnaireProgress />
  {items.map((question) => (
    <QuestionnaireItem
      key={question.name}
      name={question.name}
      required={question.required}
    >
      <QuestionnaireTitle>{question.prompt}</QuestionnaireTitle>
      <QuestionnaireDescription>
        {question.description}
      </QuestionnaireDescription>
      <QuestionnaireChoices>
        {question.choices.map((choice) => (
          <QuestionnaireChoice key={choice.value} value={choice.value}>
            <span className="font-medium">{choice.label}</span>
            {"description" in choice ? (
              <QuestionnaireChoiceDescription>
                {choice.description}
              </QuestionnaireChoiceDescription>
            ) : null}
          </QuestionnaireChoice>
        ))}
        {"input" in question ? (
          <QuestionnaireInput
            aria-label={question.input.label}
            placeholder={question.input.placeholder}
          />
        ) : null}
      </QuestionnaireChoices>
      <QuestionnaireError />
    </QuestionnaireItem>
  ))}
  <QuestionnaireActions>
    <QuestionnairePrevious />
    <QuestionnaireSkip />
    <QuestionnaireNext />
    <QuestionnaireSubmit />
  </QuestionnaireActions>
</Questionnaire>
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault()
  const answers = new FormData(event.currentTarget)
  // answers.get("direction"), answers.getAll(...) for multiple items.
}

Composition

Questionnaire
โ”œโ”€โ”€ QuestionnaireProgress
โ”œโ”€โ”€ QuestionnaireItem
โ”‚   โ”œโ”€โ”€ QuestionnaireTitle
โ”‚   โ”œโ”€โ”€ QuestionnaireDescription
โ”‚   โ”œโ”€โ”€ QuestionnaireChoices
โ”‚   โ”‚   โ”œโ”€โ”€ QuestionnaireChoice
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ QuestionnaireChoiceDescription
โ”‚   โ”‚   โ””โ”€โ”€ QuestionnaireInput
โ”‚   โ””โ”€โ”€ QuestionnaireError
โ””โ”€โ”€ QuestionnaireActions
    โ”œโ”€โ”€ QuestionnairePrevious
    โ”œโ”€โ”€ QuestionnaireSkip
    โ”œโ”€โ”€ QuestionnaireNext
    โ””โ”€โ”€ QuestionnaireSubmit

Questionnaire owns the ordered items, active item, answer state, validation, progress, and navigation. The containing page, card, dialog, or drawer owns close and cancellation behavior, persistence, transport, and branching.

Server rendering

Pass items to server-render the active item, progress, actions, and answer shortcuts.

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.

"use client"

import * as React from "react"

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.

"use client"

import * as React from "react"

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.

"use client"

import * as React from "react"

Shortcuts

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

What should the agent do next?

Use the displayed shortcut or navigate with the keyboard.

"use client"

import * as React from "react"

Custom validation

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

How much detail should the answer include?

Choose the response depth.

1 / 2
"use client"

import * as React from "react"

Controlled

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

Current checkpoint: Change scope

Question 1 of 3
What may the agent change?

The host stores the active checkpoint while Questionnaire navigates.

"use client"

import * as React from "react"

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.

"use client"

import * as React from "react"

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.

"use client"

import * as React from "react"

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

Question 1 of 2
What may the agent modify?

Next is intentionally disabled until an answer is selected.

"use client"

import * as React from "react"

Custom progress

Use the Progress render state to build a custom progress indicator.

Checkpoint 1 of 4
How large is the change?
"use client"

import * as React from "react"

Animated items

Animate the active item while keeping progress and navigation stationary.

Question 1 of 3
What should the agent do?

Choose the task for this run.

"use client"

import * as React from "react"

Card

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

What should the agent work on?
Choose the task that should be handled next.
Question 1 of 2
"use client"

import * as React from "react"

Dialog

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

"use client"

import * as React from "react"

Accessibility

QuestionnaireItem renders a fieldset, and QuestionnaireTitle renders its legend. Descriptions and active errors are associated with the current item, and invalid items and answer controls expose aria-invalid.

Fixed choices preserve native radio and checkbox behavior. Progress is exposed as a named progressbar, navigation uses real buttons, and inactive items and actions are hidden and inert. Successful navigation focuses the newly active item; failed validation focuses an available answer control.

Always give QuestionnaireInput an accessible name with a visible label, aria-label, or aria-labelledby. A placeholder is not a label.

API Reference

Each part wraps the matching primitive from the package installed above, adds a data-slot and its styles, and passes other props through.

Questionnaire

Renders a form. Accepts all form props except value and defaultValue.

PropTypeDefault
itemsQuestionnaireItemDefinition[]-
itemstring-
defaultItemstring-
onItemChange(item: string) => void-
shortcuts"letters" | "numbers"-
onSubmit(event: FormEvent) => void-

QuestionnaireItem

Renders a fieldset for one question.

PropTypeDefault
namestring-
requiredboolean-
multipleboolean-
invalidboolean-
onStatusChange(status: "unanswered" | "answered" | "skipped") => void-

QuestionnaireChoice

A radio or checkbox answer with its label. Use QuestionnaireChoiceDescription inside it for a second line.

PropTypeDefault
valuestring-
checkedboolean-
defaultCheckedboolean-
disabledboolean-
onChangeChangeEventHandler<HTMLInputElement>-

QuestionnaireChoiceDescription

A span for secondary text inside QuestionnaireChoice. Accepts all span props.

QuestionnaireInput

A text input for another answer. Accepts input props except form and name. Needs an accessible name.

QuestionnaireProgress, QuestionnaireTitle, QuestionnaireDescription, QuestionnaireChoices, QuestionnaireError

Render a div, legend, p, div and p. Accept className and a render prop.

QuestionnaireActions

A layout div for the navigation buttons. Accepts all div props.

QuestionnairePrevious, QuestionnaireSkip, QuestionnaireNext, QuestionnaireSubmit

Navigation buttons. They take Button variant and size and show a default label when they have no children.

Partvariant defaultsize defaultLabel
QuestionnairePrevious"secondary""default"Previous
QuestionnaireSkip"ghost""default"Skip
QuestionnaireNext"default""default"Next
QuestionnaireSubmit"default""default"Submit