Radio

input.radio[type="radio"]

Radio — one choice out of several, built on <input type="radio">.

Examples

One question, one answer

View “One question, one answer” as
Billing period
<fieldset class="field">
  <legend class="field-label">Billing period</legend>
  <div class="field field-inline">
    <input class="radio" id="demo-plan-m" name="demo-plan" type="radio" value="m" checked />
    <label class="field-label" for="demo-plan-m">Monthly</label>
  </div>
  <div class="field field-inline">
    <input class="radio" id="demo-plan-y" name="demo-plan" type="radio" value="y" />
    <label class="field-label" for="demo-plan-y">Yearly — two months free</label>
  </div>
  <div class="field field-inline">
    <input class="radio" id="demo-plan-c" name="demo-plan" type="radio" value="c" disabled />
    <label class="field-label" for="demo-plan-c">Custom — contact sales</label>
  </div>
</fieldset>

The <fieldset> and its <legend> are the component, not decoration around it.

The legend names the question; without it a screen reader reads "Monthly, radio button, 1 of 3" with nothing to attach it to. The shared name is what makes the three one group — drop it and each becomes an independent control that can never be unchecked. Both are markup, and no CSS can supply either.

The larger target

View “The larger target” as
Delivery
<fieldset class="field">
  <legend class="field-label">Delivery</legend>
  <div class="field field-inline">
    <input class="radio radio-lg" id="demo-ship-s" name="demo-ship" type="radio" value="s" checked />
    <label class="field-label" for="demo-ship-s">Standard — 3 to 5 days</label>
  </div>
  <div class="field field-inline">
    <input class="radio radio-lg" id="demo-ship-e" name="demo-ship" type="radio" value="e" />
    <label class="field-label" for="demo-ship-e">Express — next day</label>
  </div>
</fieldset>

.radio-lg is 28px. There is no small size, for the reason there is no .check-sm: below 24px the control fails SC 2.5.8, and a class whose only effect is a WCAG failure is a defect rather than an option.

Keyboard behaviour is entirely the platform's — one Tab stop for the group, arrow keys between the options, wrapping at both ends — which is why this component ships no JavaScript.

Tokens 10

Level 2, declared on .radio itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by radio
TokenDefault
--radio-markurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='7' fill='%23000'/%3E%3C/svg%3E")
--radio-bgvar(--color-surface-sunken)
--radio-border-colorvar(--color-border-strong)
--radio-border-widthvar(--border-width)
--radio-checked-bgvar(--color-primary)
--radio-checked-border-colorvar(--color-primary)
--radio-invalid-border-colorvar(--color-danger)
--radio-mark-colorvar(--color-on-primary)
--radio-mark-size100%
--radio-size1.5rem

Variants and states

Variants

  • .radio-lg

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :checked
  • :disabled
  • :focus
  • :hover
  • :user-invalid

Before you ship it

What you have to do 4 requirements

  1. Give every radio in one question the same name. That attribute is what makes them a group; without it each one is an independent control that can never be unchecked.
  2. Wrap the group in a <fieldset> with a <legend>. The legend names the question. Without it a screen reader reads the options with no idea what they answer, and nothing in CSS can supply one.
  3. Label every radio<label for> or a wrapping <label>.
  4. Check one by default, or accept that the group can be submitted empty. A radio group with no initial selection is a legitimate choice; a group that is required and has none is the one that needs a validation message.

src/css/components/radio.css · npx mostlycss add radio