Radios

The same box as the checkbox, drawn round, with a ::before dot that scales in from zero.

base/form-radio.css mirrors the checkbox almost rule for rule, with one structural difference: the dot is a real element that scales, not a mask that appears. Largely inspired by KNACSS.

[type='radio'] {
  appearance: none;
  border-radius: var(--radio-border-radius);
  box-shadow: inset 0 0 0 var(--radio-border-width) var(--radio-color-border);
  display: inline-grid;
  height: var(--checkable-size, 1.25em);
  place-content: center;
  width: var(--checkable-size, 1.25em);
}
TokenRole
--checkable-sizeWidth and height, falls back to 1.25em. Shared with checkbox and switch
--radio-color-backgroundTrack background
--radio-border-radiusCorner radius, a pill value in practice
--radio-border-widthBorder thickness, drawn as an inset shadow
--radio-color-borderBorder colour
--radio-color-border-hoverHover border colour
--radio-color-background-checkedDot fill
--radio-color-border-checkedDot border
--radio-border-width-checkedDot border thickness
--radio-scale-checkedDot size when checked, falls back to 0.7

The dot is always there

&::before {
  border-radius: var(--radius-pill, 9999px);
  content: '';
  scale: 0;
  /* … */
}

&:checked::before {
  scale: var(--radio-scale-checked, 0.7);
}

The ::before exists at every moment; only its scale changes. That is what makes the transition possible: scale animates, while a ::before toggled between content: none and content: '' would pop in.

place-content: center on the inline-grid parent keeps the dot centred at any scale, with no positioning maths.

em rather than rem

--checkable-size falls back to 1.25em here and to 1.25rem for the checkbox. That is a real difference: a radio sized in em follows the font size of whatever surrounds it, so a radio in a small print block shrinks with its label. Set --checkable-size explicitly if you want both controls to match exactly.

The adjacent label

[type='radio'] + label {
  --label-padding-block: var(--radio-label-padding-block);
  --label-padding-inline: var(--radio-label-padding-inline);
}

[type='radio']:not(:disabled) + label {
  cursor: pointer;
}

Same contract as the checkbox: the label must be the immediately following sibling.

Grouping

Radios in the same group share a name. A group also needs a group label, which means a <fieldset> with a <legend>, styled alongside label in base/form.css:

<fieldset>
  <legend>Delivery</legend>
  <div class="form-check">
    <input type="radio" name="delivery" id="standard" value="standard">
    <label for="standard">Standard</label>
  </div>
  <div class="form-check">
    <input type="radio" name="delivery" id="express" value="express">
    <label for="express">Express</label>
  </div>
</fieldset>

Without the fieldset, a screen reader announces each option but never the question.