Checkbox · RadioGroup · Switch Atom
Three controls with one shape: the control, its label, and an optional description line — wired together, because a checkbox whose label is not clickable is a bug that ships constantly.
Usage
Billed on the 1st, cancel anytime.
Two months free, billed today.
Available on Business plans.
One message per payment, nothing else.
Turns off transforms across the interface.
Sent every Monday morning.
<RadioGroup value={plan} onValueChange={setPlan} options={[ { value: 'monthly', label: 'Monthly', description: 'Billed on the 1st.' }, { value: 'yearly', label: 'Yearly', description: 'Two months free.' }, ]}/>
<Checkbox label="Email me receipts" description="One message per payment." defaultChecked /><Switch justified label="Reduce motion" description="Turns off transforms." />Which one
| Control | Use it when |
|---|---|
| Checkbox | Zero or more of several things, or a single opt-in that is saved with a form |
| RadioGroup | Exactly one of a small, visible set |
| Switch | A setting that applies the moment it is flipped |
Do
Use a Switch only when the change takes effect immediately — that is what a switch promises.
Don't
Put a Switch in a form with a Save button; a person who flips it and leaves believes it was applied.
Segmented
RadioGroup takes variant="segmented" to draw itself as one track of adjoining cells
rather than a column of dots. It is the same radiogroup: same role, same arrow keys, same
value — only the drawing changes (ADR 0014).
<RadioGroup variant="segmented" label="Kind" value={kind} onValueChange={setKind} options={[ { value: 'food', label: 'Food' }, { value: 'activity', label: 'Activity' }, ]}/>This is the control for two to four options — the band where a Select costs a press to
reveal choices that would have fitted on screen anyway. See
the table on Select's page for where the boundary sits and why.
Do
Keep segmented labels to a word or two — the cells share a track and the longest one sets the width.
Don't
Use segmented past four or five options; it stops fitting and becomes a Select.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
| label | ReactNode | — | Required on all three. It is the accessible name, and it is clickable. |
| description | ReactNode | — | Second line, wired through aria-describedby. |
| checked / defaultChecked | boolean | — | Checkbox and Switch. |
| indeterminate | boolean | — | Checkbox only — the "some of the children" state. |
| options | Array<{ value, label, description?, disabled? }> | — | RadioGroup only. |
| direction | 'column' | 'row' | 'column' | RadioGroup layout. |
| justified | boolean | false | Switch only: label left, control right — the settings-row arrangement. |
| onCheckedChange / onValueChange | (value) => void | — | One argument, always the value. |
Accessibility
- The label is a real
<label for>, so clicking the text toggles the control. - A description is linked with
aria-describedby, not stuffed into the label. - A
RadioGroupis aradiogroup; give it alabelwhen it is not inside aField. - The Switch thumb still moves under reduced motion — the movement is the state. Only the easing goes.