Radio

A single choice from a set, with every option visible at once. A group that shows its options instead of hiding them in a field.

Accessibility

What Radio does for you, and what it leaves to the page around it.

What it renders

Every option is a real <input type="radio"> inside its own <label>, with the option's text in a span beside it. The input is painted transparent and the dot you see is CSS drawn on top, but the control underneath is the browser's own.
  • The label wraps the input, so the option's text names it with no htmlFor and no aria-label of yours.
  • Radio.Group renders a div with role="radiogroup", so the grouping is announced as well as enforced.
  • optionType="button" changes the paint, not the DOM. A row of radio buttons is still radios in labels, so it keeps the keyboard behavior of radios.

The shared name is the keyboard behavior

The arrow keys work because every input in the group has the same name, which tells the browser they are one choice. Radio.Group generates that name and gives it to every child, whether or not you pass one.
  • One tab stop for the whole group. Tab lands on the checked option, and tabbing again leaves the group.
  • Arrow keys move the selection inside the group and wrap at the ends. They change the value as they go, as native radios do.
  • Position is announced (“radio button, 2 of 4”) because the browser knows how many share the name.
  • Loose radios lose all of it. A <Radio> outside a group gets no name from anywhere, so it is a group of one: no arrow keys, no position, and nothing stopping a sibling from being checked at the same time. Wrap them in a group, or give each the same name yourself.
  • Inside a Form.Item the generated name is seeded from the item's own name path, so two fields on one page cannot collide into a single browser group.

Naming the options and naming the group

The options are named by their text. The group has no name of its own, so a reader hearing “Standard, radio button, 2 of 4” does not know 2 of 4 what.
  • In a form, the field label does the job. Put the group in a Form.Item with a label, and the question above the options is the group's name.
  • Outside a form, aria-label or aria-labelledby on the group works, since aria-* and data-* props land on the div with role="radiogroup". Point aria-labelledby at the heading that already asks the question rather than writing the question twice.
  • An option's title is not its name. It lands on the label as a browser tooltip: it does not survive touch, and it is not what the option gets announced as. Put the words in the option.
  • Keep the option words parallel and short. They are read one at a time and out of context, so “Standard / Express / Overnight” carries where “Standard / Get it faster / Tomorrow before noon” does not.
  • An option built from options with a node label is named by that node's text. Anything the label paints without text (a swatch, a bare glyph) contributes nothing, so keep a word in there.

Disabled semantics

  • A disabled radio is a disabled input, so it is out of the tab order and arrow keys skip it. That suits one option, but disabling the whole group puts the question out of a keyboard user's reach.
  • Prefer disabling one option. When the whole set is unavailable, show the answer as text instead of a group nobody can focus.
  • A disabled option needs its reason nearby. A Tooltip around the option still opens on hover and focus of the surrounding markup, but a tooltip alone does not reach touch: put the reason in the surrounding copy when it matters.
  • Do not let gray text be the only thing saying “unavailable”.

Contrast, focus and motion

  • The focus ring is drawn by the component. The real input is transparent, so an outline is drawn on the dot, and on the button for the button type, when the input has :focus-visible. A restyle that removes the outline removes the only focus indicator there is.
  • Check the selected state twice on the button type: the outline style puts the accent ink on the surface, the solid style puts the surface ink on the accent, and those are two different pairs.
  • The dot alone carries the selection on the default type, and it is small. Keep its fill against the ring and the ring against the page, and do not rely on a color change in the label to say what the dot already says.
  • Every color comes from the active theme, so contrast depends on the theme. After changing the seed colors, check text against 4.5:1, since nothing warns when it falls below. The Tokens tab lists which tokens this component reads.
  • The radio ripples on click and the dot animates. Both are short and decorative, and turn off with ThemeScope theme={{ token: { motion: false } }}, and anything you animate on top of them belongs behind motion-safe:.