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

Requirements (testable)

Requirement: Mutual exclusion and non-empty selection {#req-radio-mutual-exclusion}

A radio group SHALL enforce that its N options are mutually exclusive so that at most one option is selected at any time, and once the user has interacted with the group it SHALL NOT be possible to return to a zero-selected state.

Scenario: Selecting a second option clears the first

  • GIVEN a radio group named "theme" with options Light, Dark, and System
  • AND the option Dark is currently selected
  • WHEN the user selects the option Light
  • THEN Light becomes the only selected option
  • AND Dark becomes unselected
  • AND exactly one option in the group is selected

Scenario: Selection cannot be emptied after first interaction

  • GIVEN a radio group where the user has already selected one option
  • WHEN the user activates that same already-selected option again
  • THEN the option remains selected
  • AND the group still reports exactly one selected option (never zero)

Requirement: Visual state rendering {#req-radio-states}

A radio button SHALL render its outer ring and inner dot according to its selection and interaction state: an unselected control SHALL show a text-muted 2 px ring with no inner dot, a selected control SHALL show an accent 2 px ring with an accent inner dot, a disabled control SHALL render all elements at 38% opacity, and an error control SHALL use the error ring color.

Scenario: Unselected versus selected rendering

  • GIVEN a radio control in the unselected state
  • WHEN the control is first rendered
  • THEN its outer ring uses the text-muted color at 2 px and no inner dot is visible
  • WHEN the control becomes selected
  • THEN its outer ring uses the accent color at 2 px
  • AND an accent-filled inner dot becomes visible

Scenario: Disabled control is dimmed

  • GIVEN a radio control marked disabled
  • WHEN the control is rendered
  • THEN the outer ring and inner dot are drawn at 38% opacity

Requirement: Accessible grouping semantics {#req-radio-grouping}

Radio buttons SHALL be grouped by a native <fieldset> with a <legend>, or equivalently by role="radiogroup" with aria-labelledby referencing a heading, so that the mutual-exclusion relationship and the group name are exposed to assistive technology.

Scenario: Fieldset groups the options

  • GIVEN a set of radio inputs sharing the same name attribute
  • WHEN they are rendered as a group
  • THEN they are enclosed in a <fieldset> element containing a <legend> that names the group
  • AND every option carries an associated visible label via <label for> or a wrapping <label>

Requirement: Keyboard selection and navigation {#req-radio-keyboard}

Radio selection SHALL be operable by pointer and keyboard: activating a radio, its label, or its row SHALL select the option; arrow keys SHALL move focus within the group and select the focused option; and Tab SHALL move focus into and out of the group as a whole rather than between individual options.

Scenario: Arrow key moves and selects within the group

  • GIVEN a vertical radio group with the second option focused and selected
  • WHEN the user presses the Down arrow key
  • THEN focus moves to the third option
  • AND the third option becomes selected
  • AND the second option becomes unselected

Scenario: Clicking the label selects the option

  • GIVEN an unselected radio option paired with a text label
  • WHEN the user clicks the label text
  • THEN the associated radio option becomes selected

Scenario: Tab exits the group rather than stepping through options

  • GIVEN a radio group with one option focused among four options
  • WHEN the user presses Tab
  • THEN focus leaves the radio group entirely and moves to the next focusable control outside the group

Requirement: Native input and screen-reader announcement {#req-radio-a11y}

A radio control SHALL be built from a native <input type="radio"> rather than a custom <div>, SHALL present a visible focus indicator, and SHALL announce its role, position within the group, label, and selection state to screen readers.

Scenario: Focused option is announced with position and state

  • GIVEN an accessible radio group of four options with the second option unselected
  • WHEN the user moves keyboard focus onto the second option
  • THEN a visible focus indicator is shown on that option
  • AND the screen reader announces it as a radio, "2 of 4", with its label and its not-selected state

References