Radio buttons

mandatory

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, accent fill, 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 4

Horizontal (compact, when options are short):

○ Yes   ● No   ○ Maybe

Vertical 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:

  • Preselect the most likely / safe default (most users) — prefills

    checked=true on 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> OR role="radiogroup"
  • Label association via <label for> OR wrapped
  • Visible focus indicator
  • Arrow key navigation native to <input type="radio"> within same

    name

  • 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
  • interaction/selection.kmd — single-select patterns
  • interaction/states.kmd — state visuals
  • foundations/ux-writing.kmd — label content style
  • themes/color-roles.kmd — accent + error tokens

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/radio-buttons.kmd