Radio button group
A labelled set of options where exactly one can be chosen. Use it when the options are few enough to show at once; when more than one can be chosen use checkboxes, and when the list is long use a select.
Live preview
Rendered by the component itself from the same generated tokens as the rest of the site.
<RadioButtonGroup
name="shipping"
label="Shipping speed"
options={[
{ value: 'standard', label: 'Standard' },
{ value: 'express', label: 'Express' },
{ value: 'overnight', label: 'Overnight' },
]}
value="standard"
onChange={setSpeed}
/>Installation
Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.
npx shadcn@latest add https://www.graphite-ui.com/r/radio-button-group.jsonimport { RadioButtonGroup, type RadioOption } from '@/components/ui/radio-button-group'Anatomy
The group label is a legend and each option carries its own label. Both are required: the option labels say what each choice is, and the legend says what the question was.
- Option labelRequiredOne per option.
- Group labelRequiredThe control renders it itself. There is no wrapper to take it from — the kit builds label text into each form control.
- Supporting textOptionalHelp, warning or error text, 8 below the options. The kit calls this Helper / Warning / Error text and builds it into the control the same way; warning and error lead with the shared 16px status icon.
Variants
Orientation is the main layout choice. Vertical is the default and scans best; horizontal suits two or three short options that fit on one line. Control position puts the radio after its label, the kit’s Position Right.
States
Focus is a pseudo-class in the code and a variant in the kit, so the page forces it here on the first option. The kit draws no hover state and the code has none. Disabled works on the whole group or on one option, and a disabled selection keeps its dot. In error every option’s ring turns danger, as the kit draws its Invalid group, and the selected dot stays on-surface. Warning leaves the rings alone and puts the status icon on the message. Read-only quietens the rings but not the answer.
API reference
Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.
| Prop | Type | Default | Notes |
|---|---|---|---|
| orientation | 'vertical' | 'horizontal' | — | The kit's Horizontal axis. Options sit 8 apart down the page and 16 apart across it. |
| controlPosition | 'left' | 'right' | — | The kit's Position on the Radio button set, which names the side the radio sits on. Right draws the label first. |
| disabled | — | — | Per-option or group-level. A disabled selected option keeps its dot, in the disabled tone. |
| readOnly | boolean | — | The kit's Read-only group. The radios stay focusable, the group carries aria-readonly, and a change is never reported, so the selection cannot move. |
| label | — | — | Required. There is no shape in which this control exists unlabelled, and no wrapper left to supply one. |
| helpText | — | — | Supporting copy. Suppressed while errorText or warningText is present. |
| warningText | — | — | Resolves the kit's Warning group, outranked by an error. |
| errorText | — | — | Its presence resolves the error state, so error text and error styling cannot be shown apart. This was Field's guarantee and it survives Field. |
Design tokens
Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.
on-surfaceThe ring, the selected dot, the option labels and warning text. The kit binds the radio glyph to icon-primary, which resolves to onSurface, not to primary.primaryThe focus ring, through the family's focus step, 2px outside the 20px frame; and the disabled and read-only tone, through its disabled-content step.on-surface-variantGroup label and helper text, which the kit binds to onSurfaceVariant rather than onSurface.dangerError ring on every option, as the kit's Invalid group draws it, the error text, and the error status icon's triangle. One role for all three, so they cannot drift apart.warningThe warning status icon's triangle.on-warningThe "!" on the warning status icon.backgroundThe "!" on the error status icon.textOption labels at Body/3.spacingThe gap between options, the label-to-radio gap, and the message offset.
Usage
The contract's prohibitions, written as the choices you will actually face.
Do
- Give the group a label that states the question (“Shipping speed”), so each option can be one word.
- Keep horizontal for two or three short options. Once they wrap, the reader cannot tell which row a label belongs to.
- Disable a single option with option.disabled when only that choice is unavailable, and the whole group when the question does not apply.
- Put errorText on the group. The message belongs to the question, not to one of the answers.
Don’t
- Rely on the option labels alone. Without the legend a screen reader hears “Express” and no question.
- Keep exclusivity in your own state. The radios share a name, so the browser already allows only one.
- Use a radio group when more than one answer can be true. That is a set of checkboxes.
- Use a radio group for a long list. Past a handful of options, a select takes less room.
Accessibility
What the component does for you, and what it leaves to you.
- Keyboard
- Native radios sharing a name: Tab enters the group once, the arrow keys move between options and select as they go, and Tab leaves.
- Roles
- A
fieldsetwith theradiogrouprole, labelled by itslegend. Each option is a native radio with a reallabel. Read-only carriesaria-readonly: the radios stay focusable, but the selection does not move. - Labels
- Help and error text are linked to the fieldset with
aria-describedby. Error text is announced as it appears. - Focus
- A 2px ring in
--graphite-primary-focusjust outside the 20px frame, on keyboard focus only. The hit area reaches 6px past the frame, so it stays 32px. - Disabled
- A disabled group disables every radio through the fieldset. A disabled option drops out of arrow-key order on its own.
Figma parity
The kit’s Radio button page has two sets: the single Radio button and the Radio button group. The code ships only the group, because a lone radio is not a choice. This table is where those facts are reconciled instead of quietly diverging.
| Kit axis | Values | Code | How it maps |
|---|---|---|---|
| Group: Horizontal | False · True | orientation | False is vertical, True is horizontal. |
| Selected | False · True | value | The group owns selection. An option is selected when its value matches. |
| Position | Left · Right | controlPosition | The side the radio sits on. Right draws the label first. |
| Group: State | Enabled · Invalid · Warning · Read-only | errorText · warningText · readOnly | Invalid rings every option in danger; Warning leaves the rings and leads the message with the status icon; Read-only quietens the rings and blocks changes. |
| Radio: State | Focus · Disabled | disabled | Focus is :focus-visible, a pseudo-class (governance rule 7), a 2px ring outside the frame. Disabled is the group prop or option.disabled, and dims the label too. |
| Radio: State | Skeleton | — | No counterpart by rule. |
| Glyph | icon-primary ring and dot | — | The ring and the 8px dot are on-surface, as the kit binds them. They were primary until 2.1.0. |
| Group label | Inside the first option (vertical) | legend | The kit’s vertical groups draw the label in the first option rather than on the group, a slip with the same pixels. The code keeps one legend. |
