ChoiceCard
Preview appears after the next Storybook update.
<ChoiceCard />When to use
Use it for
- A decision where each option needs a line of explanation or a value to compare: resolving a conflict, picking a starting brand.
- Two to four options. Side by side (horizontal, the default) when the reader compares them; they stack on their own when they no longer fit.
- vertical when the descriptions are long or the options are a ranked list.
- Put a TokenValue in description when the choice is between token values, so the reader sees what each card keeps.
- hideLabel when the row around the group already asks the question; the legend stays the accessible name.
Not for
- Options a short label explains on its own: use RadioGroup.
- Switching a view or a mode that applies at once: use SegmentedControl.
- Several options the reader can pick together: use Checkbox.
- More than four options: use RadioGroup or Select.
Do and don't
Every example is rendered with the real component, so it always matches what ships.
Titles are the choice, descriptions are the consequence
The title says what the reader picks in a few words, the description says what happens or what it keeps.
Ask the question in the legend
The legend names the decision, as a question or a noun, so the cards read as answers.
Cards for one-word options
When the titles say it all and nothing needs comparing, cards take room for nothing. Use RadioGroup.
Descriptions that repeat the title
"Keep mine: keeps your value" says it twice. Show the value instead, or what changes.
Accessibility
- A fieldset with a legend around a Base UI RadioGroup: the legend names the group, each card is a radio named by its title.
- Each card's description is linked to its radio with aria-describedby, so screen readers read it after the title. The group description and error are linked to the group.
- Tab enters the group on the selected card and leaves it; arrow keys move the selection.
- The selected card shows the checked radio, a Deep Harbor edge and the selected surface: never color alone.
- A disabled card is skipped by the arrow keys and cannot be picked; say why in its description.
- The focus ring (focus.ring.color) shows on the radio on keyboard focus only.
Content guidelines
- Legend: the decision, as a question or a noun, sentence case: "Which value should this change keep?", "Starting brand".
- Title: a verb or a short name, a few words: "Keep mine", "Use the library's", "Ocean".
- Description: one or two lines, the consequence or the value. No period after a lone value.
- Error: what to do, not what went wrong: "Pick a value before you add it to the library."
From the code Generated from the component, not editable here.
Anatomy and tokens
Read from packages/react/src/components/ChoiceCard/ChoiceCard.module.css at build time. Every value is a token: open one to see its alias chain in both themes.
- Grid of cards (Base UI RadioGroup)
.group - Side by side, stacks when it no longer fits (default)
.horizontal - One option: the whole card is the click target
.card - One option: the whole card is the click target, hover
@media (hover: hover) .card:hover:not(:has([data-disabled])) - One option: the whole card is the click target
.card:has([data-checked]) - One option: the whole card is the click target, hover
@media (hover: hover) .card:has([data-checked]):hover:not(:has([data-disabled])) - One option: the whole card is the click target
[data-invalid] .card:not(:has([data-checked])) - Title and description column
.text - Title, names the radio
.title - Description, linked with aria-describedby
.description - One option: the whole card is the click target
.card:has([data-disabled]) - One option: the whole card is the click target
.card:has([data-disabled]) .description
From RadioGroup
ChoiceCard reuses the stylesheet of RadioGroup, so it also paints with these 27 tokens. Changing them changes both.
Motion and interaction
ChoiceCard animates on hover, check and uncheck. Try it in the preview at the top.
| When | What changes | Duration | Easing |
|---|---|---|---|
| Hover | Background and border | motion.duration.instant75 ms | motion.easing.standardsettles in place |
| Check and uncheck | Background, border and movement | motion.duration.fast150 ms | motion.easing.enterstarts fast, lands softly |
With reduced motion turned on, every change happens at once.
From the code Generated from the component, not editable here.
Props
Parsed from the TypeScript types in packages/react/src/components/ChoiceCard/ChoiceCard.tsx, so this table cannot drift from the code.
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | none | Value selected when this card is chosen. |
titlerequired | ReactNode | none | The option, as a verb or a short name: "Keep mine", "Use the library's". |
description | ReactNode | none | What choosing it means, or the value it keeps (a TokenValue). One or two lines. |
disabled | boolean | none | No description in the code yet. |
className | string | none | No description in the code yet. |
Playground
Change the props and see the real ChoiceCard update. Controls are generated from the TypeScript props; nothing here changes the system.
<ChoiceCard
value="mine"
title="Keep mine"
description="{color.harbor.600}, the value in your change."
/>Workbench
For contributors: every state with the accessibility panel, in the component workbench (Storybook). The stories double as visual test fixtures; the guidance on this page is the user documentation (open ChoiceCard in Storybook).
Embed Storybook stories here (7)
Loads from https://bridge-storybook.floriangouloubi.com (set STORYBOOK_URL at build time). If Storybook is not running, the frames stay empty.
Conflict
StartingBrand
Vertical
WithError
DisabledCard
NarrowConflict
AllStates
From the code Generated from the component, not editable here.
Changelog
- Fix
Component scan: Storybook story ids keep a camel-case title word whole (Forms/ChoiceCard is forms-choicecard), so Library previews of multi-word components open the right story
v1.0.0, Nov 11, 2026
- Fix
Stories per Figma variant for the sets whose checked state comes from their group (ChoiceCard, Radio, MenuRadioItem), with a
checkedarg the render turns into the group's value, so the Storybook measure maps each story to its variantv1.0.0, Nov 11, 2026