Skip to content

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:

RoleTypeElement
radioControlElement of type: container
labelPartElement of type: text or nested instance prop of type: string
descriptionPartElement of type: text or nested instance prop of type: string
errormessagePartElement of type: text or nested instance prop of type: string
indicatorPartElement 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:

StateEffectClassify?
checkedNatively chosen; draws the indicatorRecommended
disabledNatively disabled, enforced by the platformRecommended
requiredAnnounced as requiredRecommended
invalidAnnounced as invalidRecommended
hover / activeNative, on the control itselfRecommended, if the library styles it
focus / focus-visibleNative, on the control itselfOptional — 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:

  • control as role:radio — on the layer drawing the control’s box and nothing else
  • selected as role:indicator on the dot container inside it
  • label as role:label on a text element 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

PropTypeTierGenerated body
onChange?(e: ChangeEvent<HTMLInputElement>) => voidMUSTWired — sets checked, then calls the prop
onBlur?(e: FocusEvent) => voidSHOULDStub
name?stringSHOULDOverrides the group’s name
value?stringMUST— submitted value
checkedboolean—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:

TypeAn option within a Picker
GroupingStructural — no free-standing radio control exists, so group and its radios bind together as a picker and its options
AnnouncedLabel and selected state; double-tap chooses
indicatorNot consumed — the platform draws its own selection mark

Android

Not yet planned. Intended binding:

TypeRadioButton with Role.RadioButton
GroupingselectableGroup() on the containing group
Announced”Selected” / “not selected” and the label; double-tap chooses
indicatorConsumed — 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