Choice List
A list of answers to pick from, with an optional typed answer. Use it for preference questions and for questions an assistant asks in a chat. When the question is done, show the answer with Choice Summary.
Layouts
Stacked rows for forms and settings. Compact, numbered rows for questions inside a chat. Each works as a single (radio) or multiple (checkbox) list.
Stacked · single (radio)
Stacked · multiple (checkbox)
Compact · single (radio)
Which repository should I open the issue in?
Compact · multiple (checkbox), scrolls past ~6 rows
Which Slack channels should I summarize each morning?
Bud preference
A stacked reply-style list inside a form. Custom reveals a text field. Saving locks the list.
How should I write my replies to you?
Agent question
Compact lists inside a question card. Other answer opens a typed answer. Continue with nothing chosen shows an error; Continue or Skip shows the summary.
Which repository should I open the issue in?
Which Slack channels should I summarize each morning?
Should I send the weekly digest now?
Recommended choice and custom answer
A label ending in (Recommended) gets a badge. A stacked list can always show a typed answer after its options.
States
Disabled, busy, and error states for both layouts and the typed answer.
Preselected and disabled
Stacked error
Compact error
Typed answer error
Choice summary
The answered, expired, and cancelled lines a question collapses into. Long and multi-line answers wrap.
Your answer was recorded.
Your answer was recorded.
Your answer was recorded.
This request expired without an answer.
This request was cancelled without an answer.
Usage
import { ChoiceList, ChoiceSummary } from "@tulipfarm/sprout/choice-list";
const [style, setStyle] = useState<string[]>([]);
<ChoiceList
legend="Reply style"
options={[
{ id: "short", label: "Short answers (Recommended)" },
{ id: "detailed", label: "Detailed answers" },
]}
value={style}
onChange={setStyle}
/>
<ChoiceList
layout="compact"
multiple
legend="Choose one or more answers"
options={channels}
value={choices}
onChange={setChoices}
error={attempted && !choices.length ? "Choose an answer." : undefined}
textOption={{
id: "custom-answer",
value: text,
active: typed,
onChange: setText,
onFocus: () => setTyped(true),
error: null,
}}
/>
<ChoiceSummary prompt="Reply style" answer="Short answers" />value is always an array of option ids, for one answer or many.onChange gets the next array, so a state setter can be passed straight in.
Props
layout is "stacked" (bordered 44px rows with radio or checkbox marks) or "compact" (36px numbered rows with dividers, in a list that scrolls after about six rows). There is no separate size: each layout has one row height, and compact rows grow to 44px on touch screens.
multiple turns radios into checkboxes. In a compact single list the legend is for screen readers only, because the question above already names it. disabled locks every row, anderror shows a message under the list and marks each input invalid.
An option with recommended, or a label ending in(Recommended), gets a Recommended badge.textOption adds a typed answer after the options. A stacked list always shows it. A compact list shows it only whileactive, and moves focus into it, so pair it with an "Other answer" button whose aria-controls is thetextOption.id.
ChoiceSummary takes a prompt, an optionalanswer (leave it out for an expired or cancelled question), and optional children after the answer.
Accessibility
The list is a <fieldset> with a<legend>. Each row is a label around a real radio or checkbox input, so Tab, arrow keys, and Space work as they do for native controls. A focused row shows a 2px focus ring. Errors are linked witharia-describedby, and a typed-answer error is announced.
Colors change in 120–180ms, and check marks pop in. Both stop when reduced motion is requested. In forced colors mode checked rows and focus use system highlight colors.
Theme tokens
Rows use --sprout-input borders. Checked rows use--sprout-primary-subtle with a --sprout-focusborder, and check marks use --sprout-primary, so the opt-in TulipFarm theme tints them green. Compact numbers sit on--sprout-secondary. Errors use --sprout-danger, and the summary check uses --sprout-success.