Menu


Radio


One of several choices, only one of which can be chosen, with its label beside it and an optional line under the label.

Customize the preview


Choices
Flags
View

Options


Radio options
ChoiceValuesDefaultWhat it does
sizesmbk-radio--size-<value>mdsmmd
errorsmbk-radio--errorOn or offOff
The choice is invalid, such as a required group left empty. An empty ring takes the danger edge; a chosen one keeps its fill inside a danger ring; the control also carries aria-invalid.

Partssmbk-radio__controlsmbk-radio__circlesmbk-radio__dotsmbk-radio__labelsmbk-radio__textsmbk-radio__description

Examples


Usage

Radios sharing a name make one choice between them.

Error

A choice that cannot stand, such as a delivery the address does not allow: an empty ring takes the danger edge, the chosen one a danger ring. Say why in words beside the group.

Disabled

Dimmed, in both states; a chosen one keeps its dot.

Sizes

Two sizes.

With a description

A line under each label says more; it is read after the radio’s name, not as part of it. Under one question, put them in a field group.

Guidelines


Purpose

One choice out of a small set, all of which are visible. Choosing one un-chooses the others in its group. That exclusivity is what separates it from a checkbox.

When to use

  • Two to five choices that should all be read before choosing.

When not to use

  • More choices than fit comfortably. Use the select.
  • Choices that switch something immediately with only two states. Use the switch or the segmented control.

Usage rules

Do

  • Pre-select the most common choice when there is one, so a group is never in a no-choice state that cannot be returned to.

Don't

  • Use a single radio. A lone radio cannot be un-chosen.
Do

Offer radios as a group of two or more, so there is always another choice to move to.

Don't

A lone radio cannot be unticked once chosen; a single yes or no is a checkbox.

Do

Pre-select the most common choice, so the group never starts in a state it cannot return to.

Don't

A group with nothing chosen can never be cleared again once the reader picks one.

Anatomy

  • The block, a label element holding everything.
  • The control part, the real radio, visually hidden.
  • The circle part, the drawn ring.
  • The dot part, the filled center when chosen.
  • The label part, the text.
  • The description part, an optional line under the label that says more about the choice. With it, the label and the line stack in the text part beside the circle, which sits on the label's first line.

Behavior

Choosing fills the circle with the inverse action fill and shows the dot. Arrow keys move the choice within the group. When the field group holds the chosen value, a radio is checked when its own value is the group's, and choosing it tells the group; each radio then needs a value. Disabled grays the ring and, when chosen, fills it with the disabled foreground, so a disabled choice still shows which one it is.

Choosing moves. The fill grows into the circle from a little smaller as it fades in, and the dot pops in, growing a little past its size and settling back, while the dot of the radio chosen before shrinks away. While it is pressed, the circle gives a little. Only a change moves: a radio chosen as the page loads simply shows it. Under reduced motion the fill and the dot only fade, and the circle does not give. The motion is the stylesheet's, so markup written by hand moves the same; the circle draws its fill on its own ::before, so leave that to the circle.

Under the pointer, an empty ring darkens its edge and a chosen one darkens its fill.

With error, an empty ring takes the danger edge and a chosen one keeps its fill inside a danger ring, under the pointer too. The control carries aria-invalid. Give every radio of the group the flag, and say what is wrong in words beside the group.

Two sizes: md, a 20 ring, and sm, a 16 ring. At either size the whole row is at least 24 tall.

On a touch screen each option's row keeps its height and takes an invisible press area out to the comfortable touch target; between options stacked one under another, or set side by side, it reaches only halfway into the gap, so each option stays one press.

Accessibility


What it does for you

  • Each radio is a native radio input, so arrow keys move the choice within its group.
  • The label element gives each radio its name, and a line under the label is read after the radio's name, not as part of it.
  • In error, each control is marked invalid with aria-invalid.
  • The ring and the dot do not rely on color alone: a chosen radio shows its dot, and a disabled one still shows which is chosen.
  • In a forced-color mode, a chosen radio is filled with the highlight color and its dot is drawn in the highlight's text color.
  • On a touch screen each option's row takes an invisible press area out to the comfortable touch target, reaching only halfway into the gap between options, so each stays one press.

What you must do

  • Give radios in one group a shared name attribute; that is what makes arrow keys work and what assistive technology reads as a group.
  • Give the group a group label: put the radios in a field group, a fieldset whose legend names the group, which also gives them their shared name.
  • Tie the words that say why a group is in error to the group.

How to test

  • Tab to the group: focus lands on the chosen radio, or the first one when none is chosen. The arrow keys move and choose within the group, and Tab leaves it.
  • With a screen reader, the group is read by its legend, each radio by its label, with its place in the group and whether it is chosen.
  • With text at 200% and the page 320 pixels wide, each label wraps and its row stays at least 24 tall.
  • In a forced-color mode, the chosen radio is still told from the others.

Keyboard

Radio keys
KeyWhat it does
Tab
Moves focus into the group, to the chosen radio or the first, and out of it.
Down ArroworRight Arrow
Moves to the next radio in the group and chooses it.
Up ArroworLeft Arrow
Moves to the previous radio in the group and chooses it.
Space
Chooses the focused radio.

Source


The files the radio is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.

Stylesheet
radio.css
/* smbk-radio — Radio. Figma "Radio" 4086:11110 (Size × State × Checked).
 * Choosing fills the circle from a little smaller as it fades in, and the dot pops in over it
 * (see the motion rules at the end). */

/* .smbk-radio__control: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css.
   The row, its type, the disabled colour, the label over a line under it, the focus ring on the circle and the touch area are shared
   with the other choices, in choice/choice.css; this sheet draws the circle. */

.smbk-radio__circle {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--smbk-size-selection-md);
  height: var(--smbk-size-selection-md);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-control);
  border-radius: var(--smbk-radius-full);
  background: transparent;
  position: relative;
  isolation: isolate;
}

/* The fill: a layer over the whole circle, its edge included, under the dot. It shows while the radio is chosen, when
   the circle itself draws nothing, so the two are never drawn over each other. */
.smbk-radio__circle::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: calc(-1 * var(--smbk-border-width-hairline));
  border-radius: inherit;
  background: var(--smbk-color-action-inverse-background);
  opacity: 0;
}

.smbk-radio__dot {
  width: var(--smbk-size-6);
  height: var(--smbk-size-6);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-color-action-inverse-foreground);
}

.smbk-radio__control:checked + .smbk-radio__circle {
  border-color: transparent;
}

.smbk-radio__control:checked + .smbk-radio__circle::before {
  opacity: 1;
}

/* Hover: an empty ring darkens its edge; a chosen one darkens its fill. */
@media (hover: hover) {
  .smbk-radio:hover .smbk-radio__control:enabled + .smbk-radio__circle {
    border-color: var(--smbk-color-border-control-hover);
  }

  .smbk-radio:hover .smbk-radio__control:enabled:checked + .smbk-radio__circle {
    border-color: transparent;
  }

  .smbk-radio:hover .smbk-radio__control:enabled:checked + .smbk-radio__circle::before {
    background: var(--smbk-color-action-inverse-background-hover);
  }
}

/* Error: an empty ring takes the danger edge; a chosen one keeps its fill, to its edge, inside a danger ring.
   Two classes, so the error stays under the pointer. The control marked aria-invalid="true" draws the same. */
.smbk-radio.smbk-radio--error .smbk-radio__control:enabled + .smbk-radio__circle,
.smbk-radio .smbk-radio__control[aria-invalid="true"]:enabled + .smbk-radio__circle {
  border-color: var(--smbk-color-feedback-danger-border);
}

.smbk-radio.smbk-radio--error .smbk-radio__control:enabled:checked + .smbk-radio__circle,
.smbk-radio .smbk-radio__control[aria-invalid="true"]:enabled:checked + .smbk-radio__circle {
  border-color: transparent;
  box-shadow: 0 0 0 var(--smbk-border-width-emphasis) var(--smbk-color-feedback-danger-border);
}

.smbk-radio__control:focus-visible + .smbk-radio__circle {
  background: var(--smbk-color-surface-canvas);
}

.smbk-radio__control:checked:focus-visible + .smbk-radio__circle {
  background: transparent;
}

/* Disabled, chosen or not, the circle draws itself in the disabled colours, with no fill over it. */
.smbk-radio__control:disabled + .smbk-radio__circle {
  border-color: var(--smbk-color-border-control-disabled);
  background: transparent;
}

.smbk-radio__control:disabled + .smbk-radio__circle::before {
  content: none;
}

.smbk-radio__control:disabled:checked + .smbk-radio__circle {
  padding: var(--smbk-space-inset-xs);
}

.smbk-radio__control:disabled:checked + .smbk-radio__circle .smbk-radio__dot {
  width: 100%;
  height: 100%;
  background: var(--smbk-color-action-disabled-foreground);
}

/* ---- small (sm) ---- */

.smbk-radio--size-sm .smbk-radio__circle {
  width: var(--smbk-size-selection-sm);
  height: var(--smbk-size-selection-sm);
}

.smbk-radio--size-sm .smbk-radio__dot {
  width: var(--smbk-size-4);
  height: var(--smbk-size-4);
}

/* ---- motion: choosing fills the circle and pops the dot in. The fill grows from a little smaller than
   the circle as it fades in, with the fill purpose, while the circle's own edge fades under it; the dot
   grows from nothing a little past its size and settles back, with the pop purpose, and shrinks away the
   same way when another radio is chosen. The circle gives a little while it is pressed, with the feedback
   purpose. Only a change moves: a radio chosen as the page loads is simply chosen. Under reduced motion
   the fill and the dot only fade, and nothing gives. ---- */

.smbk-radio__circle {
  transition-property: border-color, background-color, box-shadow, scale;
  transition-duration: var(--smbk-motion-fill-duration), var(--smbk-motion-fill-duration), var(--smbk-motion-fill-duration), var(--smbk-motion-feedback-duration);
  transition-timing-function: var(--smbk-motion-fill-easing), var(--smbk-motion-fill-easing), var(--smbk-motion-fill-easing), var(--smbk-motion-feedback-easing);
}

.smbk-radio:active .smbk-radio__control:enabled + .smbk-radio__circle {
  scale: 0.97;
}

.smbk-radio__circle::before {
  scale: 0.7;
  transition-property: opacity, scale;
  transition-duration: var(--smbk-motion-fill-duration);
  transition-timing-function: var(--smbk-motion-fill-easing);
}

.smbk-radio__control:checked + .smbk-radio__circle::before {
  scale: none;
}

.smbk-radio__dot {
  opacity: 0;
  scale: 0;
  transition-property: opacity, scale;
  transition-duration: var(--smbk-motion-pop-duration);
  transition-timing-function: var(--smbk-motion-pop-easing);
}

.smbk-radio__control:checked + .smbk-radio__circle .smbk-radio__dot {
  opacity: 1;
  scale: none;
}

@media (prefers-reduced-motion: reduce) {
  .smbk-radio__circle {
    transition-property: border-color, background-color, box-shadow;
  }

  .smbk-radio:active .smbk-radio__control:enabled + .smbk-radio__circle,
  .smbk-radio__circle::before,
  .smbk-radio__dot {
    scale: none;
  }

  .smbk-radio__circle::before,
  .smbk-radio__dot {
    transition-property: opacity;
  }
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  /* Chosen: the circle filled with Highlight and the dot in HighlightText, as the design fills it. */
  .smbk-radio__control:checked + .smbk-radio__circle,
  .smbk-radio__control:checked:focus-visible + .smbk-radio__circle {
    border-color: Highlight;
    background: Highlight;
  }

  .smbk-radio__circle::before {
    content: none;
  }

  .smbk-radio__control:checked + .smbk-radio__circle .smbk-radio__dot {
    background: HighlightText;
  }

  .smbk-radio__control:disabled + .smbk-radio__circle {
    border-color: GrayText;
    background: transparent;
  }

  .smbk-radio__control:disabled:checked + .smbk-radio__circle .smbk-radio__dot {
    background: GrayText;
  }
}
Tokens it reads
Radio tokens
Token and CSSValuePreviewFoundation
space.inline.sm--smbk-space-inline-smcomfortable: 6pxcompact: 4pxchanges with densitySpacing
color.text.default--smbk-color-text-defaultlight: #232527dark: #e7ebf0changes with themeColor
typography.body-sm--smbk-typography-body-sm-font-family--smbk-typography-body-sm-font-size--smbk-typography-body-sm-font-weight--smbk-typography-body-sm-line-height--smbk-typography-body-sm-letter-spacing--smbk-typography-body-sm-text-transformfontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px

The quick brown fox

Typography
size.target.min--smbk-size-target-min24pxSize
focus.ring.width--smbk-focus-ring-width2pxFocus
color.border.focus--smbk-color-border-focuslight: #cc0d0ddark: #f34336changes with themeColor
focus.ring.offset--smbk-focus-ring-offset2pxFocus
color.text.disabled--smbk-color-text-disabledlight: #7c8692dark: #606a75changes with themeColor
space.text.label--smbk-space-text-label0pxSpacing
size.selection.md--smbk-size-selection-md20pxSize
size.selection.sm--smbk-size-selection-sm16pxSize
color.text.subtlest--smbk-color-text-subtlestlight: #606a75dark: #7c8692changes with themeColor
size.target.comfortable--smbk-size-target-comfortable44pxSize
space.stack.md--smbk-space-stack-mdcomfortable: 12pxcompact: 8pxchanges with densitySpacing
border-width.hairline--smbk-border-width-hairline1pxBorder width
color.border.control--smbk-color-border-control#7c8692Color
radius.full--smbk-radius-full9999pxRadius
color.action.inverse.background--smbk-color-action-inverse-backgroundlight: #2d3237dark: #f3f5f8changes with themeColor
size.6--smbk-size-66pxSize
color.action.inverse.foreground--smbk-color-action-inverse-foregroundlight: #f9fafbdark: #232527changes with themeColor
color.border.control-hover--smbk-color-border-control-hoverlight: #606a75dark: #9ba5b1changes with themeColor
color.action.inverse.background-hover--smbk-color-action-inverse-background-hoverlight: #232527dark: #e7ebf0changes with themeColor
color.feedback.danger.border--smbk-color-feedback-danger-border#f34336Color
border-width.emphasis--smbk-border-width-emphasis2pxBorder width
color.surface.canvas--smbk-color-surface-canvaslight: #e7ebf0dark: #111213changes with themeColor
color.border.control-disabled--smbk-color-border-control-disabledlight: #c4cdd8dark: #606a75changes with themeColor
space.inset.xs--smbk-space-inset-xs4pxSpacing
color.action.disabled.foreground--smbk-color-action-disabled-foregroundlight: #7c8692dark: #606a75changes with themeColor
size.4--smbk-size-44pxSize
motion.fill.duration--smbk-motion-fill-duration320msMotion
motion.feedback.duration--smbk-motion-feedback-duration150msMotion
motion.fill.easing--smbk-motion-fill-easingcubic-bezier(0.4, 0, 0.2, 1)Motion
motion.feedback.easing--smbk-motion-feedback-easingcubic-bezier(0.16, 1, 0.3, 1)Motion
motion.pop.duration--smbk-motion-pop-duration500msMotion
motion.pop.easing--smbk-motion-pop-easingcubic-bezier(0.44, 0.85, 0.1, 1.3)Motion
Manifest
radio.manifest.json
{
  "name": "radio",
  "block": "smbk-radio",
  "summary": "One of several choices, only one of which can be chosen, with its label beside it and an optional line under the label.",
  "adapt": "css",
  "axes": {
    "size": {
      "values": [
        "md",
        "sm"
      ],
      "default": "md"
    }
  },
  "flags": {
    "error": {
      "description": "The choice is invalid, such as a required group left empty. An empty ring takes the danger edge; a chosen one keeps its fill inside a danger ring; the control also carries aria-invalid."
    }
  },
  "parts": [
    "control",
    "circle",
    "dot",
    "label",
    "text",
    "description"
  ],
  "figmaNodeId": "4086:11110"
}