Radio buttons
Single-select from N mutually exclusive options. Material parity (`/components/radio`). Use radio when N ≤ 5 and all options should be visible; use a dropdown/select for N ≥ 6.
Spec — Radio buttons
Facet *isual*do Koder Design. Material parity: https://m3.material.io/components/radio.
Anatomy
● Selected (accent ring + accent dot)
○ Unselected (text-muted ring + empty)- *uter ring* 20×20 px (default), 2 px border
- *nner dot* 10 px diameter,
accentfill, only visible when selected - *it zone* 48×48 px
- *abel*(always paired): 12 px gap,
body-medium
R1 — Cardinality
- N options in a group: ALL must be mutually exclusive
- AT LEAST one must be selectable; user can't have zero selected
after first interaction
- Default selection: indicate one as the default (sensible default
for most users) OR leave none selected with a note explaining
R2 — States (visual)
| State | Outer ring | Inner dot |
|---|---|---|
| Unselected | text-muted (2 px) |
— |
| Unselected + hover | text |
— |
| Unselected + focused | + focus ring (2 px outline) | — |
| Selected | accent (2 px) |
accent dot |
| Selected + hover | accent-strong ring |
accent-strong dot |
| Selected + focused | + focus ring | accent dot |
| Disabled | 38% opacity all | 38% opacity |
| Error | error ring |
error dot (if selected) |
R3 — Layout
Vertical (default):
○ Option 1
○ Option 2
● Option 3
○ Option 4Horizontal (compact, when options are short):
○ Yes ● No ○ MaybeVertical preferred for accessibility (each option clearly demarcated). Use horizontal only when 2-3 short options that visually fit in container.
R4 — Group semantics
Radio buttons MUST be grouped. Group declares the mutual exclusion.
<fieldset>
<legend>Theme preference</legend>
<label><input type="radio" name="theme" value="light"> Light</label>
<label><input type="radio" name="theme" value="dark"> Dark</label>
<label><input type="radio" name="theme" value="system" checked> System</label>
</fieldset>Native <fieldset> + <legend> is the canonical accessible grouping. ARIA alternative: role="radiogroup" + aria-labelledby referencing a heading.
R5 — Selection behavior
- Click on radio OR label OR row anywhere → selects
- Keyboard: arrow keys (UpDown for vertical, LeftRight for
horizontal) move within group AND select on focus
- Tab moves INTO and OUT OF the group (not between options — that's
arrows)
- Selection visually + announces "Selected" via screen reader
R6 — When NOT to use radio
| Situation | Use instead |
|---|---|
| > 5 options | Dropdown / Select |
| Independent multi-select | Checkbox |
| Toggle ON/OFF | Switch (settings) or Checkbox (form) |
| Mode picker with previews | Segmented button or card grid |
| Required selection but neutral default | Radio with no default + helper text |
R7 — Default selection
Either:
- Pre
select the most likely / safe default (most users) — prefillschecked=trueon one option - OR pre-select nothing — forces a deliberate choice
Required pre-selection when the form must submit a value (no "null" acceptable). Optional when "no preference" is meaningful.
R8 — Per-option label content
Per foundations/ux-writing.kmd:
- Short label (≤ 32 chars typical)
- Optional secondary text below (smaller, muted) for explanation
- Avoid full sentences in labels — that's body text, not control text
● Send me weekly digest
Includes top posts + replies from people you follow.
○ Send me daily highlights
Top 5 posts each morning.
○ Don't send updates
You can re-enable any time in Settings.R9 — Accessibility
<input type="radio">(NOT custom<div>)- Group via
<fieldset>ORrole="radiogroup" - Label association via
<label for>OR wrapped - Visible focus indicator
- Arrow key navigation native to
<input type="radio">within samename - Screen reader announces "Radio, 2 of 4, Dark, not selected"
R10 — Forbidden patterns
- ❌ Radio buttons that allow zero selection after first interaction
(use checkbox for "optional" semantics)
- ❌ More than 5 radio buttons (switch to dropdown)
- ❌ Mixing radio + checkbox in same logical group (semantic
contradiction)
- ❌ Radio without visible label (icon-only is rare and confusing)
- ❌ Different label widths causing column misalignment (use
consistent label width or wrap)
R11 — Per-preset variation
| Preset | Treatment |
|---|---|
material3 |
20 px outer, 10 px inner, 2 px border |
ios_cupertino |
Larger (24 px outer), thinner border |
windows_95 |
3D bevel ring + filled dot |
terminal_classic |
ASCII ( ) / (•) |
brutalist |
Sharp square radio (not round), 3 px border |
Cross-link
interaction/selection.kmd— single-select patternsinteraction/states.kmd— state visualsfoundations/ux-writing.kmd— label content stylethemes/color-roles.kmd— accent + error tokens