Build / Molecules
ChoiceGroup
A single choice from a small set, laid out as pills or tiles.
The booking flow asks the same shape of question three times — what to
discuss, how long, which team member — so this is one component rather
than three. Built on native radios in a <fieldset>, so arrow keys move
between options and the group is announced with its legend.
Reach for RadioGroup instead when the options need long descriptions
and read as a form question; reach for this when they read as a choice
the person makes at a glance.
import { ChoiceGroup } from '@meetcard/deck'Example
<ChoiceGroup label="What are you looking to discuss?" required value={topic} onChange={setTopic} options={[ { value: 'sales', label: 'Sales inquiry', hint: 'Routes to Priya Shah' }, { value: 'demo', label: 'Product demo' }, ]} />Stories
Pills, the default: a handful of short, mutually exclusive answers.
Source
const Pills = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="What are you looking to discuss?"
options={[
{ value: 'sales', label: 'Sales inquiry' },
{ value: 'demo', label: 'Product demo' },
{ value: 'partnership', label: 'Partnership' },
]}
value={value}
onChange={setValue} />
);
};An answer chosen — shown filled and announced as checked.
Source
const Selected = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="What are you looking to discuss?"
options={[
{ value: 'sales', label: 'Sales inquiry' },
{ value: 'demo', label: 'Product demo' },
{ value: 'partnership', label: 'Partnership' },
]}
value={value}
onChange={setValue} />
);
};Tiles suit comparable options that each carry a supporting line.
Source
const Tiles = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="How long do you need?"
options={[
{ value: '15', label: '15 min', description: 'Quick question' },
{ value: '30', label: '30 min', description: 'Most popular' },
{ value: '45', label: '45 min', description: 'Deep dive' },
]}
variant="tile"
value={value}
onChange={setValue} />
);
};A selected tile, which is the one place a description sits on the brand
fill rather than the page. That pairing has its own contrast floor and no
other story renders it — Tiles starts with nothing chosen, and Selected
is a pill, which has no description. The a11y check on this story is the
point of it.
Source
const SelectedTile = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="How long do you need?"
options={[
{ value: '15', label: '15 min', description: 'Quick question' },
{ value: '30', label: '30 min', description: 'Most popular' },
{ value: '45', label: '45 min', description: 'Deep dive' },
]}
variant="tile"
value={value}
onChange={setValue} />
);
};A hint explains what the choice sets in motion, and appears only once
that option is selected.
Source
const WithRoutingHint = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="What are you looking to discuss?"
options={[
{ value: 'sales', label: 'Sales inquiry', hint: 'Routes to Priya Shah.' },
{ value: 'demo', label: 'Product demo', hint: 'Routes to Marcus Lee.' },
{
value: 'partnership',
label: 'Partnership',
hint: '2 people can help with this.',
},
]}
required
value={value}
onChange={setValue} />
);
};In error, for a required choice left unanswered.
Source
const WithError = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="What are you looking to discuss?"
options={[
{ value: 'sales', label: 'Sales inquiry' },
{ value: 'demo', label: 'Product demo' },
{ value: 'partnership', label: 'Partnership' },
]}
required
error="Pick a topic to continue."
value={value}
onChange={setValue} />
);
};One option unavailable. It stays visible, so the choice does not look incomplete.
Source
const WithDisabledOption = args => {
const [value, setValue] = useState(args.value)
return (
<ChoiceGroup
label="What are you looking to discuss?"
options={[
{ value: 'sales', label: 'Sales inquiry' },
{ value: 'demo', label: 'Product demo' },
{ value: 'support', label: 'Support', disabled: true },
]}
value={value}
onChange={setValue} />
);
};Props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | ReactNode | — | — |
optionsrequired | ChoiceOption[] | — | — |
className | string | — | — |
columns | 2 | 3 | 4 | 3 | Column count for tile. Collapses to one column on narrow screens. |
description | ReactNode | — | — |
error | ReactNode | — | When present the group renders in its error state. |
hideLabel | boolean | — | Keep the label for screen readers but hide it visually. |
name | string | — | Shared radio name. Generated when omitted. |
onChange | (value: string) => void | — | — |
required | boolean | — | — |
value | string | — | value of the selected option. Leave undefined for no selection. |
variant | 'pill' | 'tile' | 'pill' | pill hugs its content for short labels that wrap freely; tile lays
the options out in equal-width columns for comparable choices. |