---
id: "packages/active-ui/questionnaire"
title: "Questionnaire"
description: "A form that asks one question at a time, with choices, text answers, progress, skipping and keyboard shortcuts."
url: "/packages/active-ui/questionnaire"
locale: "en"
revision: "e2fb6fdb7888cd9a"
---

# Questionnaire

A questionnaire walks the reader through a set of questions, one at a time: a single choice, a
multiple choice or a short text answer per step, with a progress line and buttons to go back, skip
or continue. It is one native `<form>`, so the answers arrive as form data when the last step is
submitted. It is built on the headless questionnaire of `@shadcn/react`; this package gives it the
look and leaves every word to your application.

**Example: Three questions, one at a time**

Client: Runs entirely in your browser. Source: `examples/ui/questionnaire.tsx`.

```tsx
// apps/docs/examples/ui/questionnaire.tsx
'use client';

import {useState} from 'react';

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoiceDescription,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from '@workspace/active-ui/components/primitives/questionnaire';

/**
 * Three questions, one at a time: a required single choice with number
 * shortcuts, an optional multiple choice that can be skipped, and a text
 * answer. Every word is the caller's: the button labels, the progress
 * sentence and the error. The answers are read from the form on submit; this
 * example shows them instead of sending them.
 */
export default function QuestionnaireExample() {
  const [answers, setAnswers] = useState<string | null>(null);
  return (
    <div className="w-full max-w-md">
      <Questionnaire
        shortcuts="numbers"
        onSubmit={(event) => {
          event.preventDefault();
          const data = new FormData(event.currentTarget);
          setAnswers(
            JSON.stringify(
              {
                frequency: data.get('frequency'),
                channels: data.getAll('channels'),
                remarks: data.get('remarks'),
              },
              null,
              1,
            ),
          );
        }}
      >
        <QuestionnaireProgress
          label="Questionnaire progress"
          formatProgress={({current, total}) =>
            `Question ${current} of ${total}`
          }
        />
        <QuestionnaireItem name="frequency" required>
          <QuestionnaireTitle>
            How often do you want a statement?
          </QuestionnaireTitle>
          <QuestionnaireChoices>
            <QuestionnaireChoice value="monthly">
              Every month
            </QuestionnaireChoice>
            <QuestionnaireChoice value="quarterly">
              Every quarter
            </QuestionnaireChoice>
            <QuestionnaireChoice value="yearly">
              Once a year
              <QuestionnaireChoiceDescription>
                The minimum the law requires.
              </QuestionnaireChoiceDescription>
            </QuestionnaireChoice>
          </QuestionnaireChoices>
          <QuestionnaireError>Choose how often to continue.</QuestionnaireError>
        </QuestionnaireItem>
        <QuestionnaireItem name="channels" multiple>
          <QuestionnaireTitle>Where should we send it?</QuestionnaireTitle>
          <QuestionnaireDescription>
            Choose any number, or skip the question.
          </QuestionnaireDescription>
          <QuestionnaireChoices>
            <QuestionnaireChoice value="email">Email</QuestionnaireChoice>
            <QuestionnaireChoice value="portal">Portal</QuestionnaireChoice>
            <QuestionnaireChoice value="letter">Letter</QuestionnaireChoice>
          </QuestionnaireChoices>
          <QuestionnaireError>
            Choose a channel or skip this question.
          </QuestionnaireError>
        </QuestionnaireItem>
        <QuestionnaireItem name="remarks">
          <QuestionnaireTitle>Anything else we should know?</QuestionnaireTitle>
          <QuestionnaireInput placeholder="Optional" />
        </QuestionnaireItem>
        <QuestionnaireActions>
          <QuestionnairePrevious>Previous</QuestionnairePrevious>
          <QuestionnaireSkip>Skip</QuestionnaireSkip>
          <QuestionnaireNext>Next</QuestionnaireNext>
          <QuestionnaireSubmit>Submit</QuestionnaireSubmit>
        </QuestionnaireActions>
      </Questionnaire>
      {answers ? (
        <pre
          className="bg-muted mt-4 rounded-md p-3 text-xs"
          aria-live="polite"
        >
          {answers}
        </pre>
      ) : null}
    </div>
  );
}
```

## Usage

```tsx
import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireError,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from '@workspace/active-ui/components/primitives/questionnaire';

<Questionnaire onSubmit={handleSubmit}>
  <QuestionnaireItem name="frequency" required>
    <QuestionnaireTitle>How often?</QuestionnaireTitle>
    <QuestionnaireChoices>
      <QuestionnaireChoice value="monthly">Every month</QuestionnaireChoice>
      <QuestionnaireChoice value="yearly">Once a year</QuestionnaireChoice>
    </QuestionnaireChoices>
    <QuestionnaireError>Choose an answer to continue.</QuestionnaireError>
  </QuestionnaireItem>
  <QuestionnaireActions>
    <QuestionnaireNext>Next</QuestionnaireNext>
    <QuestionnaireSubmit>Submit</QuestionnaireSubmit>
  </QuestionnaireActions>
</Questionnaire>;
```

## How it moves

- **One item at a time.** Only the current `QuestionnaireItem` is shown. `Next` moves to the next
  item and puts the focus on it; `Previous` goes back with the answers kept.
- **The buttons follow the position.** `Previous` is hidden on the first item, `Next` on the
  last, and `Submit` appears only on the last. `Skip` appears only on an item that is not
  `required`, and marks it as skipped.
- **Validation per item.** `Next` on a required item without an answer shows its
  `QuestionnaireError` and moves the focus to the first choice; the error disappears once the
  reader answers.
- **The answers are form data.** Each item's `name` is the field name: a single choice or a text
  answer gives one value, a `multiple` item one value per checked choice (`FormData.getAll`).
  Handle `onSubmit` as for any form.
- **Shortcuts.** With `shortcuts="numbers"` (or `"letters"`) each choice shows a key, and pressing
  it chooses that answer; in a multiple item it toggles.
- `item` and `onItemChange` control which item is shown, for example to restore a position.

## Anatomy

- `Questionnaire`: the `<form>`. `shortcuts`, `defaultItem` or `item`, and every form attribute.
- `QuestionnaireProgress`: a progressbar with your sentence.
- `QuestionnaireItem`: one question, a `<fieldset>`. `name`, `required`, `multiple`.
- `QuestionnaireTitle` (the `<legend>`) and `QuestionnaireDescription`.
- `QuestionnaireChoices` with `QuestionnaireChoice` per answer, and `QuestionnaireChoiceDescription`
  for a second line; or `QuestionnaireInput` for a text answer.
- `QuestionnaireError`: the message of an item that is not answered.
- `QuestionnaireActions` with `QuestionnairePrevious`, `QuestionnaireSkip`, `QuestionnaireNext` and
  `QuestionnaireSubmit`, styled as buttons (`variant` and `size` as on
  [Button](/packages/active-ui/button)).

## What your application provides

Every word, in your reader's language. The package has no fallback text:

- the visible label of each of the four buttons, as their children (required);
- `label` (the accessible name) and `formatProgress` (the sentence, also used as
  `aria-valuetext`) of `QuestionnaireProgress`, so the word order is yours;
- the message of each `QuestionnaireError`, worded for a required or an optional item;
- what happens on submit: storing or sending the answers.

## Accessibility and keyboard

- Each item is a `<fieldset>` named by its `<legend>`; single choices are radio buttons, multiple
  choices checkboxes, under the visible indicator.
- `Tab` moves through the choices and the buttons; the arrow keys move within a single choice.
- The progress is a `role="progressbar"` with your sentence as its value text.
- After `Next`, the focus moves to the new item, so a screen reader announces the new question.

## API

**Questionnaire**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

Questionnaire has no props of its own.

**QuestionnaireActions**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireActions has no props of its own.

**QuestionnaireChoice**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireChoice has no props of its own.

**QuestionnaireChoiceDescription**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireChoiceDescription has no props of its own.

**QuestionnaireChoices**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireChoices has no props of its own.

**QuestionnaireDescription**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireDescription has no props of its own.

**QuestionnaireError**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes |  |

**QuestionnaireInput**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireInput has no props of its own.

**QuestionnaireItem**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireItem has no props of its own.

**QuestionnaireNext**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes |  |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |

**QuestionnairePrevious**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes |  |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |

**QuestionnaireProgress**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `formatProgress` | `(progress: { current: number; total: number; }) => string` | yes | The progress as a sentence in your language and word order, such as "Question 2 of 5". It is shown and is the `aria-valuetext`. |
| `label` | `string` | yes | The accessible name of the progressbar, such as "Questionnaire progress". |

**QuestionnaireSkip**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes |  |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |

**QuestionnaireSubmit**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | yes |  |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |

**QuestionnaireTitle**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

QuestionnaireTitle has no props of its own.

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Questionnaire documentation of shadcn/ui (MIT licence) to
> this package; see [Third-party notices](/third-party-notices).
