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
| Part | Role | Source |
|---|---|---|
| Container | Fill, unchecked | --nm-radio-bg |
| Border | Color, unchecked | --nm-radio-border |
| Border | Color, checked | --nm-radio-checked-border |
| Bullet | Fill | --nm-radio-checked-bullet |
| Bullet | Size | 16px, centered in a 24px container |
| Container | Size / radius | 24×24px, border-radius: 999px (fixed — no size modifiers) |
| Label | Typography | .nm-choice-field__label — 16px / 400 / Open Sans |
| Focus ring | Color | --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).
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
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.
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
Both previews are locked — they don't follow the page theme switcher.
Do & don't
name when they're meant to be a group — they'll both stay selectable at once.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
readonlyhas no effect on radios. Pair.is-readonlywithtabindex="-1"andaria-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
Token reference
| Token | Role |
|---|---|
| --nm-radio-bg | Container fill, unchecked |
| --nm-radio-border | Border, unchecked |
| --nm-radio-checked-border | Border, checked |
| --nm-radio-checked-bullet | Inner bullet fill, checked |
| --nm-radio-hover-bg | Fill on hover |
| --nm-radio-hover-border | Border on hover |
| --nm-radio-checked-hover-bullet | Bullet fill on hover, checked |
| --nm-radio-readonly-bg | Fill, read-only |
| --nm-radio-readonly-border | Border, read-only |
| --nm-radio-readonly-bullet | Bullet fill, read-only + checked |
| --nm-choice-field-label-fg | Label text color |
| --nm-choice-field-required | Required mark color |
| --nm-choice-field-optional | Optional 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
| Class | Values | Default |
|---|---|---|
nm-radio | Base class — fixed 24×24px, no size modifiers | — |
:checked | Selection = on | unchecked |
[readonly] / .is-readonly | Read-only — pair with tabindex="-1" | — |
nm-choice-field__required | Marks a field as required | — |
nm-choice-field__optional | Marks a field as optional | — |