Skip to content

Radio buttons

components specs/components/radio-buttons.kmd

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.

When this spec applies

Primary triggers

All triggers

Specification body

Spec — Radio buttons

Facet Visual do Koder Design. Material parity: https://m3.material.io/components/radio.

Anatomy

●  Selected     (accent ring + accent dot)
○  Unselected   (text-muted ring + empty)
  • Outer ring: 20×20 px (default), 2 px border
  • Inner dot: 10 px diameter, accent fill, only visible when selected
  • Hit zone: 48×48 px
  • Label (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)

StateOuter ringInner dot
Unselectedtext-muted (2 px)
Unselected + hovertext
Unselected + focused+ focus ring (2 px outline)
Selectedaccent (2 px)accent dot
Selected + hoveraccent-strong ringaccent-strong dot
Selected + focused+ focus ringaccent dot
Disabled38% opacity all38% opacity
Errorerror ringerror 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 (Up/Down for vertical, Left/Right 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

SituationUse instead
> 5 optionsDropdown / Select
Independent multi-selectCheckbox
Toggle ON/OFFSwitch (settings) or Checkbox (form)
Mode picker with previewsSegmented button or card grid
Required selection but neutral defaultRadio with no default + helper text

R7 — Default selection

Either:

  • Pre-select the most likely / safe default (most users) — pre-fills 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

PresetTreatment
material320 px outer, 10 px inner, 2 px border
ios_cupertinoLarger (24 px outer), thinner border
windows_953D bevel ring + filled dot
terminal_classicASCII ( ) / (•)
brutalistSharp 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

References