One of several choices, only one of which can be chosen, with its label beside it and an optional line under the label.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
sizesmbk-radio--size-<value> | mdsm | md | |
errorsmbk-radio--error | On or off | Off | 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. |
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.
Offer radios as a group of two or more, so there is always another choice to move to.
A lone radio cannot be unticked once chosen; a single yes or no is a checkbox.
Pre-select the most common choice, so the group never starts in a state it cannot return to.
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
| Key | What 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
/* 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
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| 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-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| size.target.min--smbk-size-target-min | 24px | Size | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.text.disabled--smbk-color-text-disabled | light: #7c8692dark: #606a75changes with theme | Color | |
| space.text.label--smbk-space-text-label | 0px | Spacing | |
| size.selection.md--smbk-size-selection-md | 20px | Size | |
| size.selection.sm--smbk-size-selection-sm | 16px | Size | |
| color.text.subtlest--smbk-color-text-subtlest | light: #606a75dark: #7c8692changes with theme | Color | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size | |
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.control--smbk-color-border-control | #7c8692 | Color | |
| radius.full--smbk-radius-full | 9999px | Radius | |
| color.action.inverse.background--smbk-color-action-inverse-background | light: #2d3237dark: #f3f5f8changes with theme | Color | |
| size.6--smbk-size-6 | 6px | Size | |
| color.action.inverse.foreground--smbk-color-action-inverse-foreground | light: #f9fafbdark: #232527changes with theme | Color | |
| color.border.control-hover--smbk-color-border-control-hover | light: #606a75dark: #9ba5b1changes with theme | Color | |
| color.action.inverse.background-hover--smbk-color-action-inverse-background-hover | light: #232527dark: #e7ebf0changes with theme | Color | |
| color.feedback.danger.border--smbk-color-feedback-danger-border | #f34336 | Color | |
| border-width.emphasis--smbk-border-width-emphasis | 2px | Border width | |
| color.surface.canvas--smbk-color-surface-canvas | light: #e7ebf0dark: #111213changes with theme | Color | |
| color.border.control-disabled--smbk-color-border-control-disabled | light: #c4cdd8dark: #606a75changes with theme | Color | |
| space.inset.xs--smbk-space-inset-xs | 4px | Spacing | |
| color.action.disabled.foreground--smbk-color-action-disabled-foreground | light: #7c8692dark: #606a75changes with theme | Color | |
| size.4--smbk-size-4 | 4px | Size | |
| motion.fill.duration--smbk-motion-fill-duration | 320ms | Motion | |
| motion.feedback.duration--smbk-motion-feedback-duration | 150ms | Motion | |
| motion.fill.easing--smbk-motion-fill-easing | cubic-bezier(0.4, 0, 0.2, 1) | Motion | |
| motion.feedback.easing--smbk-motion-feedback-easing | cubic-bezier(0.16, 1, 0.3, 1) | Motion | |
| motion.pop.duration--smbk-motion-pop-duration | 500ms | Motion | |
| motion.pop.easing--smbk-motion-pop-easing | cubic-bezier(0.44, 0.85, 0.1, 1.3) | Motion |
Manifest
{
"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"
}