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.
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 questionnaireCopies 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.jsonWorks, 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
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
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.
| Prop | Type | Default |
|---|---|---|
questionsrequiredThe questions, in order. Plain data: { id, type, title, description?, options?, required? }. Type is single, multiple, text or scale. | QuestionnaireQuestion[] | none |
onSubmitrequiredCalled 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) => void | none |
onCancelShows a Cancel button and is called when it is pressed. | () => void | none |
defaultAnswersAnswers to start with. | QuestionnaireAnswers | none |
allowSkipShows a Skip button on questions that are not required. | boolean | false |
labelAccessible name of the group. | string | "Questionnaire" |
submitLabelText of the final button on the review step. | string | "Submit" |
isSubmittingThe answers are being sent. Disables the buttons on the review step. | boolean | false |
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