Radio group
A radio group is a set of mutually exclusive options where exactly one can be selected at a time. The whole group has a single accessible name, the user moves between options with the arrow keys, and choosing one option automatically clears the rest. Because selection is single-valued, the group as a whole acts like one form control — Tab lands on it once, not on every option in turn.
Reach for native HTML first: a set of <input type="radio"> controls
that share a name, wrapped in a <fieldset> with a
<legend>, gives you the full keyboard model, grouping, and
screen-reader semantics for free. Build the custom role="radiogroup"
widget below only when you genuinely cannot use the native control — and budget for
implementing the entire roving-tabindex keyboard model yourself.
Live demo
Tab into the group: focus lands on the selected option (or the first, if none is selected yet). Press the arrow keys to move between options — each move also selects the option it lands on — and press Space to (re)select the focused option. Only one option is in the tab order at a time, so a second Tab leaves the group entirely.
When to use
Prefer the native control first
- Use a radio group when the user must pick exactly one option from a small, fixed set of mutually exclusive choices that are all visible at once.
- If more than a handful of options compete for space, or the list is long, a
<select>may serve better. - When several options can be chosen independently, use checkboxes, not radios.
- Whenever you can, build the group from native
<input type="radio">controls inside a<fieldset>with a<legend>; only fall back to the ARIA pattern below when custom rendering forces it.
Markup
Author the group and its options statically with the ARIA wiring in place: the group
names itself with aria-labelledby, every option carries
aria-checked, and exactly one option is tabbable
(tabindex="0") while the rest are -1. Then
radio-group.js manages the roving tabindex and selection.
<div class="ewa-radiogroup" role="radiogroup"
aria-labelledby="rg-l" data-radiogroup>
<span class="ewa-radiogroup__label" id="rg-l">Contrast level to target</span>
<div class="ewa-radiogroup__options">
<span class="ewa-radio" role="radio" aria-checked="false" tabindex="-1">AA (minimum)</span>
<span class="ewa-radio" role="radio" aria-checked="true" tabindex="0">AA (recommended)</span>
<span class="ewa-radio" role="radio" aria-checked="false" tabindex="-1">AAA (enhanced)</span>
</div>
</div>
<!-- Preferred whenever possible: the native control -->
<fieldset>
<legend>Contrast level to target</legend>
<label><input type="radio" name="contrast" value="aa-min"> AA (minimum)</label>
<label><input type="radio" name="contrast" value="aa" checked> AA (recommended)</label>
<label><input type="radio" name="contrast" value="aaa"> AAA (enhanced)</label>
</fieldset>
Keyboard interactions
The group is a single tab stop. Once focus is inside, the arrow keys move and select in one motion, and Space selects the option that already has focus.
| Key | Action |
|---|---|
| Tab | Moves focus into the group, landing on the checked option (or the first option if none is checked). A second Tab moves focus out of the group. |
| Down Arrow / Right Arrow | Moves focus to the next option and checks it; wraps from the last option to the first. |
| Up Arrow / Left Arrow | Moves focus to the previous option and checks it; wraps from the first option to the last. |
| Space | Checks the focused option (and unchecks the previously checked one). |
| Home / End | Optional: moves focus to and checks the first / last option. |
ARIA roles, states, and properties
The container owns the group role and its accessible name; each option owns the radio role and its checked state. A roving tabindex keeps the group a single tab stop.
| Attribute | On | Purpose |
|---|---|---|
role="radiogroup" |
Container | Marks the wrapper as a single-select group of radios. |
aria-labelledby |
Container | Gives the group its accessible name from the visible group label
(or use aria-label if there is no visible text). |
role="radio" |
Each option | Marks each item as one selectable radio within the group. |
aria-checked |
Each option | true on the one selected option, false on every other. |
tabindex="0" |
One option | Exactly one option is tabbable — the checked one, or the first when none is checked — so the group is a single tab stop. |
tabindex="-1" |
Other options | Keeps the remaining options out of the tab order; the arrow keys move focus between them programmatically (roving tabindex). |
Common mistakes
Pitfalls to avoid
- Reinventing the native control. Prefer
<input type="radio">inside a<fieldset><legend>. Build a customrole="radiogroup"only when you truly cannot use it — and then implement the full arrow-key roving-tabindex model, not just clicks. - Leaving every option in the tab order. A radio group is one
tab stop. Exactly one option carries
tabindex="0"; the rest are-1and are reached with the arrow keys. - Forgetting that arrows also select. In a radio group, moving with the arrow keys checks the option focus lands on — it does not just move a highlight.
- Skipping the group name. The
role="radiogroup"needs an accessible name viaaria-labelledbyoraria-label, or the options are announced without context. - Showing selection by colour alone. Pair the checked state with a non-colour cue (a filled control dot) and remap it to system colours under forced colours.