Forms & Controls
Radio Group
A set of mutually exclusive selectable options.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
radio-group-pattern.tsx
function RadioGroupPattern() {
const [selected, setSelected] = useState("standard");
const optionRefs = useRef({});
function onKeyDown(e, index) {
let nextIndex = null;
if (e.key === "ArrowDown" || e.key === "ArrowRight") nextIndex = (index + 1) % OPTIONS.length;
if (e.key === "ArrowUp" || e.key === "ArrowLeft") nextIndex = (index - 1 + OPTIONS.length) % OPTIONS.length;
if (nextIndex !== null) {
e.preventDefault();
const next = OPTIONS[nextIndex];
setSelected(next.id); // arrowing selects immediately — no separate activation step
optionRefs.current[next.id]?.focus();
}
}
return (
<div role="radiogroup" aria-label="Shipping speed">
{OPTIONS.map((option, i) => {
const checked = selected === option.id;
return (
<button
key={option.id}
ref={el => (optionRefs.current[option.id] = el)}
role="radio"
aria-checked={checked}
tabIndex={checked ? 0 : -1} // roving tabindex: only the checked option is a Tab stop
onClick={() => setSelected(option.id)}
onKeyDown={e => onKeyDown(e, i)}
>
{option.label}
</button>
);
})}
</div>
);
}Live demo
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Group wrapper | role="radiogroup" | Identifies the container as a set of mutually exclusive options, distinct from an unrelated list of buttons. |
| Group wrapper | aria-label (or aria-labelledby) | Gives the group itself an accessible name (e.g. "Shipping speed") announced when a screen reader user enters it. |
| Each option <button> | role="radio" | Overrides default button semantics so AT announces "radio button" and exposes the checked state below. |
| Each option <button> | aria-checked | Communicates which single option is currently selected; exactly one radio in the group should be true at a time. |
| Each option <button> | tabIndex (roving 0 / -1) | Only the selected option (or the first, before any selection) is in the Tab order — matching native <input type="radio"> grouping behavior, where arrow keys move between the rest. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Tab / Shift+Tab | Moves focus into or out of the whole group, landing only on the currently selected option (roving tabindex). |
| Arrow Down / Right | Moves focus to the next option AND selects it immediately — radio groups select-on-arrow, unlike tabs where activation mode is configurable. |
| Arrow Up / Left | Moves focus to the previous option AND selects it immediately. |
| Space | Selects the focused option (redundant with arrow-key selection, but expected for consistency with checkboxes). |
Focus management rules
- Only one option is ever in the Tab order at a time: the selected option, or the first option before any selection is made.
- Arrow keys move focus between options AND change the selection in the same action — there is no separate "confirm" step.
- Tabbing out of and back into the group always returns focus to the currently selected option, not the first in list order.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 1.3.1 | Info and Relationships | A | Structure and relationships conveyed visually are also programmatically determinable. |
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |