Skip to content

Questionnaire

A short flow of questions asked one at a time, for onboarding or for an assistant that needs a few details. It shows progress, keeps answers when going back, and ends with a review step where each answer can be edited.

Inputv0.1.0experimentalWCAG 2.2 AAView spec
Question 1 of 4

What best describes your role?

import { useState } from "react";
import { Questionnaire, type QuestionnaireAnswers, type QuestionnaireQuestion } from "@rdloom/react";

const questions: QuestionnaireQuestion[] = [
  {
    id: "role",
    type: "single",
    title: "What best describes your role?",
    required: true,
    options: [
      { value: "design", label: "Design", description: "Interfaces, brand and prototypes" },
      { value: "engineering", label: "Engineering", description: "Building and shipping software" },
      { value: "product", label: "Product", description: "Planning and prioritising work" },
    ],
  },
  {
    id: "goals",
    type: "multiple",
    title: "What do you want to do first?",
    description: "Pick as many as you like.",
    options: [
      { value: "invite", label: "Invite my team" },
      { value: "import", label: "Import existing projects" },
      { value: "explore", label: "Look around" },
    ],
  },
  { id: "company", type: "text", title: "What is your company called?", placeholder: "Acme Ltd" },
  { id: "comfort", type: "scale", title: "How comfortable are you with tools like this?", lowLabel: "New to it", highLabel: "Very" },
];

export default function QuestionnaireOnboardingExample() {
  const [done, setDone] = useState<QuestionnaireAnswers | null>(null);
  return (
    <div className="flex w-full justify-center">
      {done ? (
        <p className="text-sm text-[var(--rd-color-text-default)]">Thanks, you are set up. We saved {Object.keys(done).length} answers.</p>
      ) : (
        <Questionnaire label="Welcome" questions={questions} onSubmit={setDone} submitLabel="Finish setup" />
      )}
    </div>
  );
}

Installation

npx rdloom add questionnaire

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/questionnaire.json

Works, but without upgrade tracking.

Usage

import { Questionnaire, type QuestionnaireAnswers, type QuestionnaireQuestion } from "@rdloom/react";

<div className="flex w-full justify-center">
  {done ? (
    <p className="text-sm text-[var(--rd-color-text-default)]">Thanks, you are set up. We saved {Object.keys(done).length} answers.</p>
  ) : (
    <Questionnaire label="Welcome" questions={questions} onSubmit={setDone} submitLabel="Finish setup" />
  )}
</div>

Ai clarifying

Question 1 of 4

Which format should the report use?

import { useState } from "react";
import { Questionnaire, type QuestionnaireAnswers, type QuestionnaireQuestion } from "@rdloom/react";

const questions: QuestionnaireQuestion[] = [
  {
    id: "format",
    type: "single",
    title: "Which format should the report use?",
    required: true,
    options: [
      { value: "summary", label: "One-page summary" },
      { value: "detailed", label: "Detailed, with tables" },
    ],
  },
  {
    id: "period",
    type: "single",
    title: "Which period should it cover?",
    required: true,
    options: [
      { value: "month", label: "Last month" },
      { value: "quarter", label: "Last quarter" },
      { value: "year", label: "Last 12 months" },
    ],
  },
  {
    id: "include",
    type: "multiple",
    title: "What should it include?",
    options: [
      { value: "revenue", label: "Revenue" },
      { value: "refunds", label: "Refunds" },
      { value: "churn", label: "Churn" },
    ],
  },
  { id: "notes", type: "text", title: "Anything else I should know?", placeholder: "Optional" },
];

export default function QuestionnaireAiClarifyingExample() {
  const [sent, setSent] = useState<QuestionnaireAnswers | null>(null);
  return (
    <div className="flex w-full justify-center">
      {sent ? (
        <p className="text-sm text-[var(--rd-color-text-default)]">Sent to the assistant: {JSON.stringify(sent)}</p>
      ) : (
        <Questionnaire label="A few questions before I start" questions={questions} onSubmit={setSent} onCancel={() => setSent({})} submitLabel="Send answers" />
      )}
    </div>
  );
}

With skip

Question 1 of 3

What should we call you?

import { useState } from "react";
import { Questionnaire, type QuestionnaireAnswers, type QuestionnaireQuestion } from "@rdloom/react";

const questions: QuestionnaireQuestion[] = [
  { id: "name", type: "text", title: "What should we call you?", required: true },
  {
    id: "source",
    type: "single",
    title: "How did you hear about us?",
    description: "You can skip this one.",
    options: [
      { value: "friend", label: "A friend" },
      { value: "search", label: "A web search" },
      { value: "event", label: "An event" },
    ],
  },
  { id: "rating", type: "scale", title: "How was signing up?", lowLabel: "Hard", highLabel: "Easy" },
];

export default function QuestionnaireWithSkipExample() {
  const [result, setResult] = useState<QuestionnaireAnswers | null>(null);
  return (
    <div className="flex w-full justify-center">
      {result ? (
        <p className="text-sm text-[var(--rd-color-text-default)]">Saved: {JSON.stringify(result)}</p>
      ) : (
        <Questionnaire label="Quick survey" questions={questions} allowSkip onSubmit={setResult} />
      )}
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
questionsrequired

The questions, in order. Plain data: { id, type, title, description?, options?, required? }. Type is single, multiple, text or scale.

QuestionnaireQuestion[]none
onSubmitrequired

Called from the review step with the answers keyed by question id. Single choice and text give a string, multiple choice an array of values, scale a number.

(answers: QuestionnaireAnswers) => voidnone
onCancel

Shows a Cancel button and is called when it is pressed.

() => voidnone
defaultAnswers

Answers to start with.

QuestionnaireAnswersnone
allowSkip

Shows a Skip button on questions that are not required.

booleanfalse
label

Accessible name of the group.

string"Questionnaire"
submitLabel

Text of the final button on the review step.

string"Submit"
isSubmitting

The answers are being sent. Disables the buttons on the review step.

booleanfalse

Accessibility

Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab moves through the options and buttons; Arrow keys move between choices in a single choice or scale question
  • Space selects an option; Enter in a text answer moves to the next question
  • Back, Next, Skip, Cancel and Edit are buttons

Screen readers announce

  • The heading of each question is read when focus moves to it
  • The progress is announced as Question n of total
  • A missing answer is announced as an alert

What your code must do

  • Each question heading takes focus when the step changes
  • The step change is announced politely: Question 2 of 5
  • Progress is a progressbar with a value, plus the visible text Question 2 of 5
  • A required question that is empty shows an error, marks the field invalid and moves focus to the error
  • Answers are kept when going back
  • The review step lists every answer with an Edit button that names the question

Guidelines

Use it when

  • Onboarding with a few questions
  • An assistant that needs clarifying answers before it goes on
  • Any flow where one question at a time reduces effort

Avoid it when

  • Many unrelated fields: use Form
  • Long surveys with branching logic: build a dedicated flow

Don't

  • More than about seven questions
  • Marking every question required
  • Clearing answers when the reader goes back

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-default
  • --rd-color-surface-subtle
  • --rd-color-surface-selected
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-border-default
  • --rd-color-border-strong
  • --rd-color-action-primary
  • --rd-color-feedback-danger
  • --rd-color-focus-ring
  • --rd-radius-control
  • --rd-radius-overlay