Radio Group
Selects exactly one option from a set, with every option visible at once.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/radio-group.jsonUsage
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'<RadioGroup defaultValue="routine" aria-label="Priority">
<Label><RadioGroupItem value="routine" /> Routine</Label>
<Label><RadioGroupItem value="urgent" /> Urgent</Label>
</RadioGroup>Wrapping each item in a Label makes the text a click target as well as the dot, which matters
more than it sounds: an 18px circle is a poor target, and the words beside it are already where
the pointer is going.
A radio group needs a name. Put a FieldLabel above it, or an aria-label on the group when
the surrounding layout already reads as a heading. Without one, a screen reader announces four
radio buttons and never says what they are choosing between.
Radio, checkbox, or select?
| Use | When |
|---|---|
RadioGroup | One choice from two to about six, all worth seeing at once — priority, laterality, second-read policy |
Checkbox | Any number of choices, including none |
Select | One choice from a closed list too long to show, or a form dense enough that six rows of radios would swamp it |
ToggleGroup | One choice that changes the view immediately, and reads as a control rather than a form field |
A radio group cannot be emptied once set. That is the platform behaviour, not something we could change, so a group whose answer is genuinely optional needs a default or an explicit "None" option — never an unset group the reader is unable to return to.
Layout
The group is a grid with a 10px gap, stacked. For a short set of one-word options, add
grid-flow-col and a wider gap to put them on one line. Do not do this with long labels: the
distance between a label and the next option's dot shrinks until the reader has to guess which
dot belongs to which word.
Descriptions per option
The report is final once the first reader signs it.
A second radiologist reads the study before the report leaves the department.
Requires three readers. Unavailable while the department is short-staffed.
FieldItem gives each option its own label and description, indented past the dot so the
description lines up with the label rather than the control. Reach for it when the options have
consequences a reader could not guess from three words — which, for anything clinical, is most
of them.
disabled on a FieldItem disables the radio inside it and greys the whole row, so the reason
stays legible next to the option it applies to.
Disabled and read-only
Three different statements, and they are not interchangeable:
- One item disabled — that option exists but is unavailable. Keep the reason in the label; an option that silently vanishes is a worse answer than one that explains itself.
- The group disabled — this decision is not yours to make here.
readOnly— the value is settled and is being shown to you. The group stays focusable and the value stays selectable text, which a disabled group does not.
In a field
Recorded against the order and visible to the referrer.
A radio group inside a Field gets its label, description and error placement from the field.
Validation needs a hand, though: there is no blur to hang a check on when the whole group is one
tab stop, so drive the error yourself with invalid on the field and match={boolean} on
FieldError rather than waiting for a validity event that will not come.
Keyboard
Arrow keys move and select within the group, and the group is a single tab stop — Tab moves past it rather than through it. That is the platform behaviour for radios and it is why this should never be built out of buttons. A disabled item is skipped by the arrows.
Because moving the focus also changes the value, do not attach anything expensive or irreversible
to onValueChange: a keyboard user passes through every option on the way to the last one.
Styling
Checked state is drawn as a thick border rather than an inner dot, so there is one element to animate and no risk of the dot and ring disagreeing at small sizes:
.cn-radio[data-checked] { border-color: var(--background-brand-default); border-width: 5px; }The resting outline is --color-input (border-base-tertiary), the lightest token that clears
the 3:1 WCAG 2.2 SC 1.4.11 asks of a control boundary. An unchecked radio is nothing but its
boundary, so do not soften it.
API Reference
Everything Base UI's Radio Group accepts. The ones
you will reach for, on RadioGroup:
Prop
Type
On RadioGroupItem:
Prop
Type
The item renders a <span> plus a hidden <input type="radio">, which is what carries the value
into a form.