Form

Radio

Radios let a user pick exactly one option from a small, visible set.

Components · Form

A radio represents one option in a mutually exclusive set — selecting one always deselects the others sharing its name. Namirial radios are built from --nm-radio-* tokens defined in namirial-theme.css, ship at a single fixed size, and pair with the same .nm-choice-field label wrapper used by checkbox and switch.

Overview

A radio group forces a single choice among a small, fully-visible set of options. Unlike a checkbox, clicking a radio never toggles it off — it only becomes unselected when a sibling in the same group is selected. There is no mixed state: exactly one radio in a group is checked once a default has been set.

When to use

Single choice, always visible:
Exactly one choice from 2–5 mutually exclusive options (Monthly / Annual, Small / Medium / Large), shown so the user can compare them all at once before choosing.

Not a fit for:

  • More than ~5 options — use a select instead, so the list doesn't dominate the page
  • Situations where multiple options can be true at once — use checkboxes instead
  • A compact, icon-forward mode switch with 2–4 options — a segmented control reads faster

Default selection:
Pre-select a default only when a wrong or unnoticed guess is low-risk and easy to change. Leave the group unselected when the choice is sensitive, consequential, or must be an informed, deliberate decision — don't default someone into consent, data sharing, or anything they should actively opt into.

Required state:

  • "Required" applies to the whole group, not to any single radio — you can't mark one option as required, only "at least one of these must be selected." Enforcing it needs a validation message on submit (e.g. "Please select a shipping method").
  • Don't rely on that message alone: mark the group as required up front — a required indicator next to the <legend> — so the user knows before they submit, not just after failing to.

Anatomy

A radio is a single <input type="radio"> styled with appearance: none. The selected state is a solid inner circle scaled in from 0 to 1 — there's no icon mask involved, unlike checkbox's checkmark.

Tokens and values that drive each part

PartRoleSource
ContainerFill, unchecked--nm-radio-bg
BorderColor, unchecked--nm-radio-border
BorderColor, checked--nm-radio-checked-border
BulletFill--nm-radio-checked-bullet
BulletSize16px, centered in a 24px container
ContainerSize / radius24×24px, border-radius: 999px (fixed — no size modifiers)
LabelTypography.nm-choice-field__label — 16px / 400 / Open Sans
Focus ringColor--palette-focus-700 (light) / --palette-focus-300 (dark), 2px outline, 2px offset

Selection

A radio has only two states — there's no mixed/indeterminate state, since a radio group always resolves to exactly one selection (or none, before a default is chosen).

off
unselected
on
selected

Radio is fixed at 24×24px — there is no --sm modifier like checkbox has.

Once a user picks an option, the group can't be cleared back to unselected on its own — clicking a radio never unchecks it, and there's no native gesture to undo the pick. If a form genuinely needs a way back to "no selection," add an explicit option for it (None, No preference) rather than expecting users to find a way around a normal radio group.

Interaction states

Default
Hover
Read-only
Focus

All shown checked

No disabled state

Radios never ship the native "greyed out" disabled look. If an option is off the table for now, use read-only so its selection state stays legible and its value still submits with the form.

Read-only implementation

readonly has no native effect on <input type="radio">. Pair .is-readonly with tabindex="-1" and aria-readonly="true" — the class handles the visual, pointer-events: none blocks clicks, and removing it from the tab order blocks keyboard activation:

<input type="radio" name="plan" class="nm-radio is-readonly" checked readonly tabindex="-1" aria-readonly="true">

Markup

Radios only behave as a mutually-exclusive set when they share the same name. Wrap the group in a <fieldset> with a <legend> so assistive tech announces what's being chosen.

Billing period

Once one radio in a name group is focused, arrow keys move selection between siblings — this roving behavior is native to <input type="radio"> and needs no JavaScript.

Light & dark theme

Light data-theme="light"
Dark data-theme="dark"

Both previews are locked — they don't follow the page theme switcher.

Do & don't

✓Do
Use radios when exactly one option applies and all options should stay visible.
✕Don't
Don't give each radio its own name when they're meant to be a group — they'll both stay selectable at once.
✓Do
A radio group needs at least two options — with nothing to compare against, there's no real choice being made.
✕Don't
Don't use a single, standalone radio — a lone option can't be unchecked and isn't a mutually exclusive set. Use a checkbox instead.

Accessibility

Semantics

  • Give every radio in a group the same name — this is what makes them mutually exclusive and is what screen readers rely on to report "2 of 4," etc.
  • Wrap the group in <fieldset> + <legend>, naming the question being answered ("Billing period," not "Options").

Keyboard

  • Tab moves to the group once, landing on the checked radio (or the first, if none is checked yet).
  • Arrow keys move selection between radios in the same group — native browser behavior, no JavaScript required.
  • Focus uses :focus-visible, so the ring appears on keyboard focus but not on mouse click.

Read-only

  • Native readonly has no effect on radios. Pair .is-readonly with tabindex="-1" and aria-readonly="true" so it's fully inert while its value still submits with the form.

Labeling

  • Never rely on color alone — the filled bullet is the primary signal, but make sure the label text also makes the selected option obvious out of context.

Playground

<label class="nm-choice-field"> <input type="radio" name="plan" class="nm-radio" checked> <span class="nm-choice-field__label">Email me updates</span> </label>

Token reference

TokenRole
--nm-radio-bgContainer fill, unchecked
--nm-radio-borderBorder, unchecked
--nm-radio-checked-borderBorder, checked
--nm-radio-checked-bulletInner bullet fill, checked
--nm-radio-hover-bgFill on hover
--nm-radio-hover-borderBorder on hover
--nm-radio-checked-hover-bulletBullet fill on hover, checked
--nm-radio-readonly-bgFill, read-only
--nm-radio-readonly-borderBorder, read-only
--nm-radio-readonly-bulletBullet fill, read-only + checked
--nm-choice-field-label-fgLabel text color
--nm-choice-field-requiredRequired mark color
--nm-choice-field-optionalOptional mark color

Implementation

No JavaScript component is required — a radio group is a set of native <input> elements sharing a name, styled by CSS classes.

<link rel="stylesheet" href="namirial-theme.css">

<fieldset>
  <legend>Billing period</legend>
  <label class="nm-choice-field">
    <input type="radio" name="billing" class="nm-radio" checked>
    <span class="nm-choice-field__label">Monthly</span>
  </label>
  <label class="nm-choice-field">
    <input type="radio" name="billing" class="nm-radio">
    <span class="nm-choice-field__label">Annual</span>
  </label>
</fieldset>

CSS classes

ClassValuesDefault
nm-radioBase class — fixed 24×24px, no size modifiers—
:checkedSelection = onunchecked
[readonly] / .is-readonlyRead-only — pair with tabindex="-1"—
nm-choice-field__requiredMarks a field as required—
nm-choice-field__optionalMarks a field as optional—