Questionnaire
A form asked one question at a time, with choices, progress and previous, next and submit.
Vibra marks a chosen answer with the muted fill and its filled indicator and drops shadcn's accent-tinted border, since in Vibra an outline around a box means focus or an error; the letter or number shortcut beside a choice is set in the 10px avatar register rather than an arbitrary 0.625rem. The navigation buttons are Vibra's Button variants. It is one form: Next will not move past an unanswered required question, and only the question in view is focusable.
Install
$
npx shadcn@latest add @vibra/questionnaireNeeds the @vibra registry in your components.json — set it up once.
Examples
"use client"
import * as React from "react"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
const GOALS = [
{ value: "revenue", label: "Track revenue" },
{ value: "product", label: "Understand product usage" },
{ value: "support", label: "Run support" },
]
const TEAM_SIZES = ["Just me", "2–10", "11 or more"]
export default function QuestionnaireDemo() {
const [summary, setSummary] = React.useState<string | null>(null)
return (
<Questionnaire
shortcuts="letters"
className="w-full max-w-md gap-3"
onSubmit={(event) => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const goal = GOALS.find((item) => item.value === form.get("goal"))?.label ?? "No goal"
setSummary(`${goal}, team of ${form.get("team-size") ?? "unknown size"}.`)
}}
>
<QuestionnaireProgress />
<QuestionnaireItem name="goal" required>
<QuestionnaireTitle>What will Northwind mostly be for?</QuestionnaireTitle>
<QuestionnaireChoices>
{GOALS.map((goal) => (
<QuestionnaireChoice key={goal.value} value={goal.value}>
{goal.label}
</QuestionnaireChoice>
))}
</QuestionnaireChoices>
</QuestionnaireItem>
<QuestionnaireItem name="team-size" required>
<QuestionnaireTitle>How many people will use it?</QuestionnaireTitle>
<QuestionnaireChoices>
{TEAM_SIZES.map((size) => (
<QuestionnaireChoice key={size} value={size}>
{size}
</QuestionnaireChoice>
))}
</QuestionnaireChoices>
</QuestionnaireItem>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireNext />
<QuestionnaireSubmit>Build my dashboard</QuestionnaireSubmit>
</QuestionnaireActions>
<p role="status" className="text-sm text-muted-foreground empty:hidden">
{summary}
</p>
</Questionnaire>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| shortcuts | "letters" | "numbers" | — | Answers a choice by key — A, B, C or 1, 2, 3 — with the key shown beside it. |
| defaultItem / item / onItemChange | string / string / (item: string) => void | — | The question in view, by its name; pass item and onItemChange to own it. |
| QuestionnaireItem.name | string | — | The form field the question's answer is submitted under. |
| QuestionnaireItem.required / multiple | boolean / boolean | false / false | Keeps Next from moving on until it is answered; takes several choices as checkboxes instead of one. |
| QuestionnaireProgress | React.ComponentProps<"div"> | "Question N of M" | A live progressbar that reads out where the reader is. |
Dependencies
Registry
Source
"use client"
import * as React from "react"
import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire"
import { cn } from "@/lib/utils"
import { buttonVariants, type Button } from "@/components/ui/button"
import { CheckIcon } from "lucide-react"
function Questionnaire({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Root>) {
return (
<QuestionnairePrimitive.Root
data-slot="questionnaire"
className={cn("flex w-full min-w-0 flex-col gap-4", className)}
{...props}
/>
)
}
function QuestionnaireProgress({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Progress>) {
return (
<QuestionnairePrimitive.Progress
data-slot="questionnaire-progress"
className={cn(
"min-h-[1lh] w-fit min-w-[14ch] text-xs font-medium text-muted-foreground tabular-nums",
className
)}
{...props}
/>
)
}
function QuestionnaireItem({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Item>) {
return (
<QuestionnairePrimitive.Item
data-slot="questionnaire-item"
className={cn(
"flex min-w-0 flex-col gap-4 border-0 p-0 outline-none",
className
)}
{...props}
/>
)
}
function QuestionnaireTitle({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Title>) {
return (
<QuestionnairePrimitive.Title
data-slot="questionnaire-title"
className={cn(
"font-heading text-base leading-snug font-medium text-pretty [&:not(:has(~[data-slot=questionnaire-description]))]:mb-4",
className
)}
{...props}
/>
)
}
function QuestionnaireDescription({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Description>) {
return (
<QuestionnairePrimitive.Description
data-slot="questionnaire-description"
className={cn("text-sm text-pretty text-muted-foreground", className)}
{...props}
/>
)
}
function QuestionnaireChoices({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choices>) {
return (
<QuestionnairePrimitive.Choices
data-slot="questionnaire-choices"
className={cn(
"group/questionnaire-choices grid min-w-0 gap-2",
className
)}
{...props}
/>
)
}
function QuestionnaireChoice({
children,
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choice>) {
return (
<QuestionnairePrimitive.Choice
data-slot="questionnaire-choice"
className={cn(
"group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-lg border border-input bg-transparent px-3 py-2.5 text-start text-sm transition-colors outline-none select-none hover:bg-muted/50 has-[>input:focus-visible]:focus-outline data-invalid:border-destructive dark:bg-input/20 data-[checked]:bg-brand-muted",
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<QuestionnairePrimitive.ChoiceInput
data-slot="questionnaire-choice-input"
className="absolute inset-0 z-10 size-full cursor-pointer opacity-0"
/>
<span
aria-hidden="true"
data-slot="questionnaire-choice-indicator"
className="pointer-events-none relative flex size-4 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-[4px] border border-input group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[type=radio]/questionnaire-choice:rounded-full group-data-checked/questionnaire-choice:border-primary group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground dark:bg-input/30 dark:group-data-checked/questionnaire-choice:bg-primary"
>
<span
data-slot="questionnaire-choice-indicator-dot"
className="hidden size-2 rounded-full bg-primary-foreground group-data-[type=checkbox]/questionnaire-choice:hidden group-data-checked/questionnaire-choice:block"
/>
<CheckIcon data-slot="questionnaire-choice-indicator-check" className="hidden size-3.5 group-data-[type=radio]/questionnaire-choice:hidden group-data-checked/questionnaire-choice:block" />
</span>
<QuestionnairePrimitive.ChoiceLabel
data-slot="questionnaire-choice-label"
className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug"
>
{children}
</QuestionnairePrimitive.ChoiceLabel>
<QuestionnairePrimitive.ChoiceShortcut
data-slot="questionnaire-choice-shortcut"
className="pointer-events-none ms-auto hidden size-5 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-md border border-input bg-background font-mono text-avatar leading-none font-medium text-muted-foreground group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[shortcut]/questionnaire-choice:inline-flex"
/>
</QuestionnairePrimitive.Choice>
)
}
function QuestionnaireChoiceDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="questionnaire-choice-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
function QuestionnaireInput({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Input>) {
return (
<div
data-slot="questionnaire-input-wrapper"
className="group/questionnaire-input relative w-full min-w-0"
>
<QuestionnairePrimitive.Input
data-slot="questionnaire-input"
className={cn(
"h-8 min-h-11 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-[color,box-shadow,background-color] focus-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 sm:min-h-0 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground",
className
)}
{...props}
/>
</div>
)
}
function QuestionnaireError({
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Error>) {
return (
<QuestionnairePrimitive.Error
data-slot="questionnaire-error"
className={cn("mt-2 text-sm text-destructive", className)}
{...props}
/>
)
}
function QuestionnaireActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="questionnaire-actions"
className={cn(
"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 sm:min-h-8",
className
)}
{...props}
/>
)
}
function QuestionnairePrevious({
children,
className,
size = "default",
variant = "outline",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Previous> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Previous
data-slot="questionnaire-previous"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-1 row-start-1 min-h-11 justify-self-start sm:min-h-0",
className
)}
{...props}
>
{children ?? "Previous"}
</QuestionnairePrimitive.Previous>
)
}
function QuestionnaireSkip({
children,
className,
size = "default",
variant = "outline",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Skip> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Skip
data-slot="questionnaire-skip"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-2 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Skip"}
</QuestionnairePrimitive.Skip>
)
}
function QuestionnaireNext({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Next> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Next
data-slot="questionnaire-next"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Next"}
</QuestionnairePrimitive.Next>
)
}
function QuestionnaireSubmit({
children,
className,
size = "default",
variant = "default",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Submit> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Submit
data-slot="questionnaire-submit"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className
)}
{...props}
>
{children ?? "Submit"}
</QuestionnairePrimitive.Submit>
)
}
export {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoiceDescription,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
}