Question
ProAn agent asking you something mid-task. A few options with a line of why, a recommended pick, room for your own answer, and a receipt that folds to one line once you reply. The answer goes back to the agent as the tool result.
Which database should I set up for the orders service?
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
A header that says who is asking, the title with the question itself, the content that folds away once you reply (options, steps, actions) and the answer receipt that opens in its place. Keep the title outside the content and it stays as the receipt's heading.
import {
Question,
QuestionHeader,
QuestionIcon,
QuestionLabel,
QuestionStepper,
QuestionTitle,
QuestionContext,
QuestionContent,
QuestionSteps,
QuestionStep,
QuestionOptions,
QuestionOption,
QuestionOther,
QuestionActions,
QuestionSkip,
QuestionBack,
QuestionSubmit,
QuestionAnswer,
} from "@/components/ui/question"
<Question>
<QuestionHeader>
<QuestionIcon />
<QuestionLabel />
<QuestionStepper />
</QuestionHeader>
<QuestionContent>
<QuestionSteps>
<QuestionStep>
<QuestionTitle />
<QuestionContext />
<QuestionOptions>
<QuestionOption />
<QuestionOther />
</QuestionOptions>
</QuestionStep>
</QuestionSteps>
<QuestionActions>
<QuestionSkip />
<QuestionBack />
<QuestionSubmit />
</QuestionActions>
</QuestionContent>
<QuestionAnswer />
</Question>Single and multiple#
type="single" is a radio group: arrows move the pick, and the row's number key turns into a check. type="multiple" is a toggle group with a checkbox per row, so it reads as “pick any” before you touch it. A picked row is a faint chip in full ink, never the brand color.
Where should the preview deploy?
Which checks should run before I open the PR?
Pick any. Each one adds about a minute.
Your own answer#
QuestionOther is the escape hatch for when none of the options fit. Picking it opens a field in place, right under the row, and the typed text goes back in place of the option. Submit stays disabled while the field is empty.
How should people sign in?
Several questions#
Wrap each question in a QuestionStep inside QuestionSteps. The header's stepper counts them, Next becomes Submit on the last one, and the frame tweens to each step's height so the change registers. onSubmit gets one key per step.
Which database?
What should ship in the first version?
Pick any. The rest go on the roadmap.
Where does it deploy?
States#
Four states. Asking is the open card. Once answered, skipped or expired, the content folds away, the icon turns into a check, a forward arrow or an hourglass, and the receipt opens in its place. Expired is yours to set, when the agent stops waiting and goes on without you.
Which database should I set up?
Which database should I set up?
You chosepostgres
Which database should I set up?
Skipped. The agent will use its best judgment.
Which database should I set up?
No answer in time. The question expired.
Receipts#
A flow's receipt is a row per step, named by the step's label. Pass the stored answer as defaultValue with status="answered" to rebuild it from chat history; the options mount folded, so the receipt still reads their labels.
You choseNo answer
In a chat#
Inside a Chat turn, use variant="plain". It drops the frame, so the question reads as part of the answer, and the header can go too: the message already says who is asking.
Density#
Like Plan and Tool, Question reads the density context (or a density prop). It tunes the frame's padding and the option rows, which follow DropdownMenu's heights at each density.
Which database?
Which database?
Keyboard#
Arrow keys move between options. 1 to 9 pick the option with that number from anywhere in the question, outside the text field. Enter on an option or in the Other field submits, or goes to the next step. Space toggles a row in a multiple question. Typing on a picked Other row jumps into its field.
API reference#
Question
The root: status, the answers and the step on show, which every part reads. Every part accepts className, merged last, and every part is droppable. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "asking" | "answered" | "skipped" | "expired" | - | Controlled status. QuestionSubmit and QuestionSkip move it; set expired yourself when the agent stops waiting. |
| defaultStatus | QuestionState | "asking" | Initial status, uncontrolled. |
| onStatusChange | (status: QuestionState) => void | - | Called on submit (answered) and skip (skipped). |
| value | Record<string, string[]> | - | Controlled selection, keyed by step value (or "answer" without steps). A picked Other is the QUESTION_OTHER marker. |
| defaultValue | Record<string, string[]> | - | Initial selection, uncontrolled. Pass the stored answer to rehydrate a receipt. |
| onValueChange | (value: Record<string, string[]>) => void | - | Called on every pick. |
| defaultOther | Record<string, string> | - | Initial Other text per key. |
| step | string | - | Controlled step on show, by QuestionStep value. |
| defaultStep | string | - | Initial step. The first step when omitted. |
| onStepChange | (step: string) => void | - | Called on Next and Back. |
| onSubmit | (answers: Record<string, string[]>) => void | - | The tool result. Option values per key, with the typed text in place of Other. |
| onSkip | () => void | - | Called when the person skips. |
| variant | "card" | "plain" | "card" | card frames the question; plain drops the frame for a Chat turn. |
| density | "comfortable" | "compact" | - | Padding and option row height. Inherits the nearest DensityProvider (compact with none). |
| asChild | boolean | false | Render your own element as the root. |
| Data attribute | Description |
|---|---|
| data-status | The question's status. |
QuestionHeader
The top line: icon, label, stepper. Forwards native <div> props.
QuestionIcon
A 16px glyph that morphs to a check, a forward arrow or an hourglass when the question is answered, skipped or expired. Defaults to the question mark. aria-hidden.
QuestionLabel
Who is asking, muted. Renders a <span>.
QuestionStepper
2/3 in tabular-nums, the current number rolling as you step. Renders nothing for a single question or once answered.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | (current: number, total: number) => string | Question ${current} of ${total} | The accessible wording. |
QuestionTitle
The prompt. Labels the options it belongs to. Outside QuestionContent it stays in the receipt as its heading. Renders an <h3>.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Render your own heading level. |
QuestionContext
Why the agent asks, muted, tucked under the title. Describes the options. Renders a <p>.
QuestionContent
Everything that folds away once answered: options, steps, actions. Inert when folded. Forwards native <div> props.
| Data attribute | Description |
|---|---|
| data-state | open while asking, closed after. |
QuestionSteps
Holds the steps of a flow and tweens its height between them, so each step lands at its own size. Forwards native <div> props.
QuestionStep
One question of a flow. Only the active one shows; it slides in from the side you are heading. A role="group" <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The key its answer is stored under. |
| label | string | - | Short name for the receipt row (Database). Falls back to value. |
| Data attribute | Description |
|---|---|
| data-active | Present on the step on show. |
| data-direction | forward or back, after the first move. |
QuestionOptions
The choices. A Radix RadioGroup for single, a Radix ToggleGroup for multiple: arrows move, number keys pick, Enter submits.
| Prop | Type | Default | Description |
|---|---|---|---|
| type | "single" | "multiple" | "single" | One answer, or any number. |
| name | string | - | Overrides the key. Defaults to the step's value, or "answer". |
| loop | boolean | true | Arrow keys wrap around. |
QuestionOption
One choice, on DropdownMenu's row anatomy. Its number key sits on the right and turns into a check when picked; multiple shows a checkbox instead. Renders a <button>.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | What goes back to the agent. |
| description | React.ReactNode | - | One line on what the choice means. |
| recommended | boolean | string | false | Adds a Recommended chip, or your own wording. |
| textValue | string | - | The receipt's wording when the children are not a plain string. |
| Data attribute | Description |
|---|---|
| data-selected | Present when picked. |
QuestionOther
The last row: your own answer. Picking it reveals a field in place, which takes focus. Its text replaces the marker in onSubmit.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Other" | The row's label. |
| description | React.ReactNode | - | One line under the label. |
| placeholder | string | "Type your answer" | The field's placeholder. |
| inputProps | React.ComponentProps<"input"> | - | Forwarded to the field. |
| Data attribute | Description |
|---|---|
| data-state | open while the field shows. |
QuestionActions
The button row. Skip sits on the left, Back and Submit on the right. Forwards native <div> props.
QuestionSkip
A ghost Skip. Moves the status to skipped. Forwards Button props.
QuestionBack
A ghost Back. Renders nothing on the first step. Forwards Button props.
QuestionSubmit
The primary action. Reads Next until the last step, then Submit, with the label rolling between them. Disabled until the step is answered.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | "Submit" | The last step's label. |
| nextLabel | React.ReactNode | "Next" | The label on every other step. |
QuestionAnswer
The receipt. Opens once the question is no longer asking: You chose Postgres, a row per step for a flow, a note when skipped or expired. A role="status" <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
| chosenLabel | React.ReactNode | "You chose" | The lead-in of a single answer. |
| skippedLabel | React.ReactNode | - | The skipped note. |
| expiredLabel | React.ReactNode | - | The expired note. |
| emptyLabel | React.ReactNode | "No answer" | Shown for a step left blank. |
| children | React.ReactNode | - | Your own receipt instead. |
| Data attribute | Description |
|---|---|
| data-state | open once answered, skipped or expired. |