radio
radio declares that an element is one option in a mutually exclusive set. Without it the option cannot be focused, chosen, submitted, or validated, its label is an unassociated sibling, and nothing makes it exclusive with its siblings — a set behaves as unrelated toggles.
Status — React: Specified • Web Components: Specified • iOS: Not yet planned • Android: Not yet planned
Roles
Apply the following roles to elements:
| Role | Type | Element |
|---|---|---|
radio | Control | Element of type: container |
label | Part | Element of type: text or nested instance prop of type: string |
description | Part | Element of type: text or nested instance prop of type: string |
errormessage | Part | Element of type: text or nested instance prop of type: string |
indicator | Part | Element of type: container |
The role element becomes the control itself. label, description, and errormessage are lifted out as siblings wired by id. The indicator — the inner dot — is consumed into styling rather than emitted as an element, so a glyph-typed indicator cannot be used here and warns; use checkbox’s pattern instead.
Unannotated descendants do not render. A bare wrapper drops silently; anything carrying content of its own is dropped and named in a warning.
States
The radio role is typically applied in conjunction with the following states:
| State | Effect | Classify? |
|---|---|---|
checked | Natively chosen; draws the indicator | Recommended |
disabled | Natively disabled, enforced by the platform | Recommended |
required | Announced as required | Recommended |
invalid | Announced as invalid | Recommended |
hover / active | Native, on the control itself | Recommended, if the library styles it |
focus / focus-visible | Native, on the control itself | Optional — prefer the platform default |
All of these land on the element the stylesheet already targets, because the control and the visual are one element — no focus indicator to re-draw, no state to mirror onto a wrapper.
There is no indeterminate; that belongs to checkbox, and classifying it here warns. A library spelling the chosen state selected classifies it as checked — the prop keeps its name, and the concept decides the mechanism.
Read more about states in specs.
Specs
Component anatomy typically has elements and roles like:
anatomy: root: type: container control: type: container role: radio selected: type: container role: indicator formLabel: type: instance instanceOf: formLabel role: [label]Figma
Annotate the following layers:
controlasrole:radio— on the layer drawing the control’s box and nothing elseselectedasrole:indicatoron the dotcontainerinside itlabelasrole:labelon atextelement or through a nested instance
The label, description, and error layers are siblings of the control container, not descendants. Where the layer tree offers no box-only container, restructure the layers — the alternative is warnings and a control missing its chrome.
React
Implementation
<FormGroup header={<Legend>Cabin class</Legend>} name="cabin-class"> <Radio value="economy" label="Economy" /> <Radio value="premium" label="Premium" defaultChecked /></FormGroup>Outside a group, name it directly:
<Radio name="cabin-class" value="economy" label="Economy" checked={choice === "economy"} onChange={() => setChoice("economy")} />Before / After
// before<div className="radio" data-element="root" aria-disabled={p.disabled ? 'true' : undefined} aria-selected={p.selected ? 'true' : undefined}> <div className="radio__control" data-element="control"> <div className="radio__selected" data-element="selected" /> </div> <FormLabel {...defaults} data-element="formLabel" /></div>
// after<div className="radio" data-element="root"> <input id={controlId} className="radio__control" data-element="control" type="radio" name={p.name ?? group?.name} value={p.value} checked={selected === "selected"} onChange={(e) => { setSelected(e.target.checked ? "selected" : "unselected"); p.onChange?.(e); }} disabled={p.disabled} /> <FormLabel {...defaults} data-element="formLabel" htmlFor={controlId} /></div>The drawn dot becomes a rule rather than an element:
.radio__control { appearance: none; /* the drawn box: size, border, radius, fill */ }.radio__control::before { /* the drawn dot, from the indicator element */ }.radio__control:checked::before { opacity: 1 }Three elements become one, aria-selected is gone, and name arrives from the group through context.
Contract
| Prop | Type | Tier | Generated body |
|---|---|---|---|
onChange? | (e: ChangeEvent<HTMLInputElement>) => void | MUST | Wired — sets checked, then calls the prop |
onBlur? | (e: FocusEvent) => void | SHOULD | Stub |
name? | string | SHOULD | Overrides the group’s name |
value? | string | MUST | — submitted value |
checked | boolean | — | The existing variant prop |
Web Components
Same <input type="radio"> in the shadow root, styled the same way. Two differences:
- Grouping needs no context. Slotted children are real descendants of the group’s host, so the radio walks up on connect and reads its name. Re-resolves on reconnect.
- An id reference does not cross a shadow boundary. A label rendered by a different component cannot associate by id. Accessible text recovers through
partText(); click-the-label-to-choose does not.
iOS
Not yet planned. Intended binding:
| Type | An option within a Picker |
| Grouping | Structural — no free-standing radio control exists, so group and its radios bind together as a picker and its options |
| Announced | Label and selected state; double-tap chooses |
indicator | Not consumed — the platform draws its own selection mark |
Android
Not yet planned. Intended binding:
| Type | RadioButton with Role.RadioButton |
| Grouping | selectableGroup() on the containing group |
| Announced | ”Selected” / “not selected” and the label; double-tap chooses |
indicator | Consumed — RadioButton accepts colors, so box and dot transfer |
Additional details
What the platform supplies
Behavior a grouped set gets for free, none of which is markup:
- choosing one option clears the others
- arrow keys move between options and wrap at the ends
- the whole set is one stop in the focus order
- the chosen value participates in submission and validation
Wired state
onChange follows the wired state model: uncontrolled between prop changes, prop wins on change. Prerequisite: a checked classification in the states convention naming the prop, or the handler degrades to a stub and warns.
It matters more here than anywhere else, because a radio cannot un-choose itself — only a sibling becoming chosen clears it, and that sibling is a different component instance. A consumer owning the set should drive checked from its own state and treat onChange as the report.
Grouping
A spec describes one radio and cannot see its siblings, so grouping comes from the containing group — including through a slot, the usual shape.
No group and no explicit name warns. Two ungrouped sets on one page silently become one set, and choosing in the second clears the first.
Accessible name
From the label part, emitted as a real label element wired to the control. Where the label is an instance, the part routes rather than emits. A radio with no resolving label warns — an unnamed option is announced as “2 of 3” with nothing about what choosing it means.
See also
- group — what makes a set of radios exclusive
- checkbox — the proxy pattern, and when a drawn indicator needs it
- indicator — the drawn dot, consumed into styling here
- Roles overview — the vocabulary and how roles are authored