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)

Reply style

Stacked · multiple (checkbox)

Multiple choices

Compact · single (radio)

Which repository should I open the issue in?

Choose one answer

Compact · multiple (checkbox), scrolls past ~6 rows

Which Slack channels should I summarize each morning?

Choose one or more answers

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?

Reply style

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?

Choose one answer

Which Slack channels should I summarize each morning?

Choose one or more answers

Should I send the weekly digest now?

Choose one answer

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.

Reply style
Reply styleShort answers

States

Disabled, busy, and error states for both layouts and the typed answer.

Preselected and disabled

Preselected and disabled
Saving answer (busy)

Stacked error

Reply style

Choose a reply style.

Compact error

Choose one answer

Choose an answer.

Typed answer error

Reply style

Choice summary

The answered, expired, and cancelled lines a question collapses into. Long and multi-line answers wrap.

Which repository should I open the issue in?tulipfarm/web (Recommended)

Your answer was recorded.

Which Slack channels should I summarize each morning?#support-escalations, #eng-releases, #incidents

Your answer was recorded.

Anything I should know before drafting the launch note?Mention the pricing change first. Keep the tone calm — this ships to every workspace on Monday.

Your answer was recorded.

Should I archive last quarter's planning docs?

This request expired without an answer.

Which repository should I open the issue in?

This request was cancelled without an answer.

Reply styleShort answersSaved

Usage

Choice List usageTSX
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.