Radio
Examples
Sizes
Figma size sets: Sm 16px · Md 20px.
States
Interaction states from Figma: Default, Hover, Focus, Disabled, Read-only — across Unchecked and Checked.
Validation
Figma Validation=Invalid (Negative) — use validation="error". Unchecked uses a light negative fill; checked uses solid negative.
Radio with Label
RadioOption is Figma Radio.Option — label required, optional description.
Option Group — Vertical
Option Group — Horizontal
Controlled
Selected: email
TanStack Form
Submit without a selection to see validation. Choosing a plan and submitting again succeeds.
RTL
Wrap the control (or a parent) in dir="rtl" / lang="fa". Option labels and descriptions align to the inline-start edge.
API Reference
Radio
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique value within the group (required) |
size | "sm" | "md" | "sm" | Box size — Sm 16px / Md 20px |
validation | "none" | "error" | "none" | Figma Validation (error = Invalid Negative) |
disabled | boolean | false | Disables interaction |
readOnly | boolean | false | Prevents selection (disabled surface) |
required | boolean | false | Marks as required for form validation |
inputRef | Ref<HTMLInputElement> | — | Ref to the hidden native radio input |
className | string | — | Extra classes |
RadioOption
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Label text (required) |
description | ReactNode | — | Helper text below the label |
value | string | — | Unique value within the group (required) |
All Radio props | — | — | Passed through to the inner Radio |
RadioOptionGroup
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "vertical" | "horizontal" | "vertical" | Layout direction |
value | string | — | Controlled selected value |
defaultValue | string | — | Initial value (uncontrolled) |
onValueChange | (value: string) => void | — | Called when selection changes |
name | string | — | Form field name |
inputRef | Ref<HTMLInputElement> | — | Ref to the group’s hidden native input |
disabled | boolean | false | Disables all radios in the group |
readOnly | boolean | false | Prevents changing selection |
required | boolean | false | Marks the group as required |
Accessibility
WCAG success criteria
| Criterion | Level | How EDS helps | |
|---|---|---|---|
| 2.1.1 Keyboard | A | Arrow keys move selection within the group; Space selects; Tab enters/leaves the group | |
| 2.4.7 Focus Visible | AA | :focus-visible ring with 2px offset outside the control | |
| 1.3.1 Info and Relationships | A | radiogroup relationship plus labeled options communicate the single-choice set | |
| 4.1.2 Name, Role, Value | A | Named radiogroup, radio roles, aria-checked, and shared name — verify group + option names in QA | |
| 1.4.3 Contrast (Minimum) | AA | Label and description text use default foreground tokens | |
| 1.4.11 Non-text Contrast | AA | Circle border, selected fill, dot, and error styling meet 3:1 against adjacent colors | |
| 3.3.1 Error Identification | A | validation="error" on options plus consumer-provided error text | |
| 3.3.2 Labels or Instructions | A | RadioOption requires label; groups require aria-label / aria-labelledby | |
| 2.5.8 Target Size (Minimum) | AA | RadioOption label enlarges the hit target; avoid bare radios without labels in touch layouts |
Why it matters
Radio groups force a single choice — billing plans, shipping methods, consent levels. When the group is unnamed, arrow keys do nothing useful, or focus disappears after selection, users cannot complete forms without a mouse. Screen-reader users need the group name, the selected option, and only one checked item announced reliably. Getting radiogroup semantics wrong is one of the most common 4.1.2 failures in product audits.
Keyboard
| Key | Context | Behavior |
|---|---|---|
| ⇥ Tab / ⇧ + ⇥ Tab | Page / form | Enters or leaves the group — focus lands on the currently selected radio, or the first enabled option if none is selected |
| ↑ / ↓ | Vertical RadioOptionGroup | Moves selection to the previous/next enabled option (roving focus within the group) |
| ← / → | Horizontal RadioOptionGroup | Moves selection to the previous/next enabled option |
| Space | Focused, enabled radio | Selects the focused option (if not already selected) |
| Space / Arrow keys | disabled group or option | No change — disabled options are skipped in arrow navigation |
| Space / Arrow keys | readOnly group | No change — options stay focusable but selection cannot move |
Arrow keys operate within the group only; Tab moves between groups and other controls. Only one radio per group may be selected at a time.
Screen readers & semantics
| Surface | Semantics | Notes |
|---|---|---|
RadioOptionGroup | role="radiogroup" via Base UI RadioGroup | Must have aria-label or aria-labelledby naming the set (e.g. “Plan”, “Notifications”) |
Radio / RadioOption | role="radio" with aria-checked | Exactly one true per group; others false |
RadioOption | <label> wrapping control + text | Visible label supplies the accessible name for each option |
Bare Radio | Requires aria-label or aria-labelledby | Use inside a named group when no visible label exists |
name on group | Shared native form name | Ensures correct submission and groups options for assistive tech |
inputRef | Hidden native <input type="radio"> | Enables programmatic focus / scroll-to-field from TanStack Form |
| Dot indicator | aria-hidden="true" | Decorative — selection state comes from the radio role |
validation="error" | Visual styling only | Apply to each option (or the group’s options) and pair with role="alert" error text |
For 4.1.2 Name, Role, Value, verify three things in audits: the radiogroup has a name, each radio has a name, and the checked state updates when arrow keys or Space change selection.
Focus & visuals
- Focus ring:
:focus-visibleon each radio draws a 2px gap + 2px stroke outside the circle via::after, using--eds-color-outline-primary(overridable with--eds-component-radio-focus-ring). - Roving focus: Base UI keeps DOM focus on the active radio while arrow keys move selection — the focus ring follows the current choice.
- Contrast: Selected fill and dot use semantic tokens; error (
validation="error") states use negative tokens for 3:1 non-text contrast against adjacent backgrounds. - Target size:
RadioOptionexpands the clickable region via its<label>; bare radios should sit in layouts with adequate spacing or an associated label. - RTL: Options lay out with logical properties; arrow keys follow document reading order (inline axis), so ←/→ still move between options correctly in
dir="rtl".
Build with it
Do
- Always wrap options in
RadioOptionGroup— a loneRadiostill needs a group ancestor for roving focus andradiogroupsemantics. - Name the group with
aria-labeloraria-labelledbybefore shipping. - Set
nameonRadioOptionGroupfor native form posts and assistive tech grouping. - Pass
inputRefon the group (or option) when TanStack Form needs to focus the field after validation failure. - Apply
validation="error"to options and render descriptive error copy withrole="alert". - Use
readOnly(notdisabled) when the selected value must remain visible and focusable but not changeable.
Don't
- Render multiple independent
<Radio />elements without aRadioOptionGroup— arrow keys and radiogroup naming will break. - Rely on legend text alone without wiring
aria-labelledbyto the group. - Use separate radio groups for what is logically one question (splits keyboard navigation and confuses screen readers).
- Disable the entire group to show a read-only answer — use
readOnlyon the group instead.
Quick test
- Keyboard-only (2 min): Tab into a vertical group; use ↓/↑ to move — only one option stays selected; Space confirms; Tab out reaches the next control.
- VoiceOver / NVDA (2 min): Confirm the group name is announced on entry; each option reads “radio button, checked/unchecked, N of M”; selection updates when arrows move.
- 4.1.2 spot check (1 min): Inspect the DOM — one
radiogroupwith an accessible name, each option aradiowith its own name, exactly onearia-checked="true". - Form error (1 min): Submit the TanStack Form demo empty — error text appears, options show error styling, refocus lands on the field when using
inputRef. - RTL (30 sec): Set
dir="rtl"on a horizontal group — labels align inline-start and arrow keys still move between options in reading order.