Deck
Browse the catalog

Build / Molecules

ChoiceGroup

  • molecule
  • play-fn

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

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Pills, the default: a handful of short, mutually exclusive answers.

Pills
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.

Selected
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.

Tiles
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.

Selected Tile
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.

With Routing Hint
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.

With Error
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.

With Disabled Option
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

PropTypeDefaultDescription
labelrequiredReactNode——
optionsrequiredChoiceOption[]——
classNamestring——
columns2 | 3 | 43Column count for tile. Collapses to one column on narrow screens.
descriptionReactNode——
errorReactNode—When present the group renders in its error state.
hideLabelboolean—Keep the label for screen readers but hide it visually.
namestring—Shared radio name. Generated when omitted.
onChange(value: string) => void——
requiredboolean——
valuestring—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.

Open in Storybookfor controls, the a11y panel and the interaction runner.