A box that is ticked or not, with its label beside it and an optional line under the label, for choices that are independent of each other.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
sizesmbk-checkbox--size-<value> | mdsm | md | |
tonesmbk-checkbox--tone-<value> | defaultsubtle | default | Default is raised with a lift; subtle is sunken with the strong border, for dense places such as table rows. |
errorsmbk-checkbox--error | On or off | Off | The choice is invalid, such as terms that must be accepted. An empty box takes the danger edge; a ticked one keeps its fill inside a danger ring; the control also carries aria-invalid. |
Examples
Usage
A native checkbox with its label.
States
Unchecked and checked.
Mixed
A box over a list whose items are partly ticked shows a dash: give it indeterminate, and follow the list with it.
Error
A choice that must change before the form can be sent: an empty box takes the danger edge, a ticked one a danger ring. Say why in words beside it.
Disabled
Dimmed, in both states.
Sizes
Two sizes.
Guidelines
Purpose
A yes-or-no choice that stands on its own, or one of several that may all be chosen. The box shows the state; the label beside it says what is being chosen.
When to use
- Agreeing to something.
- Choosing any number of items from a short list.
- Switching a setting that takes effect when the form is saved.
When not to use
Usage rules
Do
- Write labels as the thing being chosen, not as a question.
- Stack related checkboxes vertically under one group label.
Don't
- Use a checkbox with no visible label and rely on an aria-label alone, except in a table row where the column header names it.
- Round the box into a circle or a capsule. Its corner is a control corner; a round mark reads as a radio.
Name the thing being chosen, so a tick clearly means yes to it.
A question as the label leaves it unclear whether a tick means yes or no.
Stack related checkboxes under one group label, so they read as one list.
Checkboxes strung along one line with no label are hard to scan and easy to miss.
Anatomy
- The block, a label element holding everything, so pressing the text toggles the box.
- The control part, the real checkbox, visually hidden but present for keyboard and assistive technology.
- The box part, the drawn square.
- The icon part, the tick, shown when checked.
- The mixed-icon part, the dash, shown when the control is mixed.
- 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 box, which sits on the label's first line.
Behavior
Two tones: the default box is raised with a lift; the subtle box is sunken with the strong border and no lift, for dense places such as table rows. Both check the same way.
Pressing the box or the label toggles it. Checked fills the box with the inverse surface and shows the tick. Mixed, the control's indeterminate state, fills it the same way and shows a dash: use it on a box that stands for a list whose items are partly ticked. Pressing a mixed box ticks it, as the browser does. The React wrapper sets the mixed state from its indeterminate prop; in plain markup a script sets the control's property. Disabled grays both box and label.
Ticking moves. The fill grows into the box from a little smaller as it fades in, and once the box has nearly filled, the tick draws itself from its start to its tip. Unticking erases the tick at once while the fill shrinks away, and the dash of a mixed box draws without waiting. A press on the way turns it round from where it is. Only a change moves: a box that is ticked or mixed as the page loads simply shows it. Under reduced motion the fill and the tick only fade. The motion is the stylesheet's, so markup written by hand moves the same; the box draws its fill on its own ::before, so leave that to the box.
Under the pointer, an empty box darkens its edge and a ticked or mixed one darkens its fill.
With error, an empty box takes the danger edge and a ticked or mixed one keeps its fill inside a danger ring, under the pointer too. The control carries aria-invalid. The color is not the message: say what is wrong in words beside it, as the field's error line does.
Two sizes: md, a 20 box, and sm, a 16 box, for dense places. At either size the whole row is at least 24 tall, so it is easy to press.
The box part can also stand alone inside another control that carries the state itself, such as a dropdown item that ticks. A real checkbox cannot sit inside a button, so there the box is drawn ticked with its checked modifier (smbk-checkbox__box--checked) instead of by a checked input.
On a touch screen a row with a real control 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
- The real control carries the state and the keyboard behavior; Space toggles it.
- The label element is the accessible name.
- The focus ring draws around the box, not the whole row, so it reads at the control.
- The mixed state is announced as mixed, and the box shows the dash. The React wrapper's
indeterminatesets the control'sindeterminateproperty after each render. - In error, the control is marked invalid.
- A line under the label is read after the checkbox's name, not as part of it.
- The tick and the dash are hidden from assistive technology; the control carries the state.
What you must do
- Give it label text, which is required. Name a box with no words beside it, such as a table row's, with
aria-labelinstead. - In plain markup, set the mixed state by script, since no attribute sets it: it is the control's
indeterminateproperty. The browser clears it at the next press, so work it out again from the list each time. - Tie the words that say why to the control or to its group when it is in error.
- Put several checkboxes under one question in a field group, whose legend names them as a group.
How to test
- Tab to the checkbox: the focus ring shows around the box, in both themes and in a forced-color mode. Press Space: it toggles.
- With a screen reader, it is read by its label as a checkbox, checked, not checked or mixed, then the line under the label. In error, it is read as invalid, with the reason.
- With a group, the legend is read with each checkbox.
- With text at 200% and the page 320 pixels wide, the label wraps and the box stays whole.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the checkbox. |
| Space | Toggles the checkbox. |
Source
The files the checkbox is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-checkbox — Checkbox. Figma "Checkbox" 4068:10770 (Size × State × Checked × Variant, the tone here).
* A real checkbox control, visually hidden, drives a drawn box beside its label.
* The default box is raised with a lift; the subtle box is sunken with the strong border, for dense places like table rows.
* Checked shows the tick; mixed (the control's indeterminate state) shows a dash instead.
* Ticking fills the box from a little smaller as it fades in, then draws the tick (see the motion rules at the end). */
/* .smbk-checkbox__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 box and the touch area are shared
with the other choices, in choice/choice.css; this sheet draws the box. */
.smbk-checkbox__box {
box-sizing: border-box;
display: inline-grid;
grid-template: minmax(0, 1fr) / minmax(0, 1fr);
place-items: center;
flex-shrink: 0;
width: var(--smbk-size-selection-md);
height: var(--smbk-size-selection-md);
padding: var(--smbk-space-inset-2xs);
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-control);
border-radius: var(--smbk-radius-control-sm);
background: var(--smbk-color-surface-raised);
color: var(--smbk-color-text-inverse);
box-shadow: var(--smbk-elevation-control);
position: relative;
isolation: isolate;
}
/* The fill: a layer over the whole box, its edge included, drawn in the ticked colours under the tick. It shows
while the box is ticked or mixed, when the box itself draws nothing, so the two are never drawn over each other. */
.smbk-checkbox__box::before {
content: "";
position: absolute;
z-index: -1;
inset: calc(-1 * var(--smbk-border-width-hairline));
box-sizing: border-box;
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-control-selected);
border-radius: inherit;
background: var(--smbk-color-surface-inverse);
opacity: 0;
}
/* The subtle tone. */
.smbk-checkbox--tone-subtle .smbk-checkbox__box {
border-color: var(--smbk-color-border-strong);
background: var(--smbk-color-surface-sunken);
box-shadow: none;
}
/* The tick and the dash share the box's one cell, so either can show without moving the other. */
.smbk-checkbox__icon,
.smbk-checkbox__mixed-icon {
grid-area: 1 / 1;
visibility: hidden;
font-size: var(--smbk-size-icon-sm);
}
/* Ticked or mixed by its control, or drawn ticked where the box stands alone inside another control
(a dropdown item that ticks), which carries the state itself: the fill shows, and the box clears its own
colours and drops its lift. */
.smbk-checkbox__control:checked + .smbk-checkbox__box,
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box,
.smbk-checkbox__box--checked {
border-color: transparent;
background: transparent;
box-shadow: none;
}
.smbk-checkbox__control:checked + .smbk-checkbox__box::before,
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box::before,
.smbk-checkbox__box--checked::before {
opacity: 1;
}
.smbk-checkbox__control:checked:not(:indeterminate) + .smbk-checkbox__box .smbk-checkbox__icon,
.smbk-checkbox__box--checked .smbk-checkbox__icon,
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box .smbk-checkbox__mixed-icon {
visibility: visible;
}
/* Hover: an empty box darkens its edge; a ticked or mixed one darkens its fill. */
@media (hover: hover) {
.smbk-checkbox:hover .smbk-checkbox__control:enabled + .smbk-checkbox__box {
border-color: var(--smbk-color-border-control-hover);
}
.smbk-checkbox:hover .smbk-checkbox__control:enabled:checked + .smbk-checkbox__box,
.smbk-checkbox:hover .smbk-checkbox__control:enabled:indeterminate + .smbk-checkbox__box {
border-color: transparent;
}
.smbk-checkbox:hover .smbk-checkbox__control:enabled:checked + .smbk-checkbox__box::before,
.smbk-checkbox:hover .smbk-checkbox__control:enabled:indeterminate + .smbk-checkbox__box::before {
background: var(--smbk-color-action-inverse-background-hover);
}
}
/* Error: an empty box takes the danger edge; a ticked or mixed 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-checkbox.smbk-checkbox--error .smbk-checkbox__control:enabled + .smbk-checkbox__box,
.smbk-checkbox .smbk-checkbox__control[aria-invalid="true"]:enabled + .smbk-checkbox__box {
border-color: var(--smbk-color-feedback-danger-border);
}
.smbk-checkbox.smbk-checkbox--error .smbk-checkbox__control:enabled:checked + .smbk-checkbox__box,
.smbk-checkbox.smbk-checkbox--error .smbk-checkbox__control:enabled:indeterminate + .smbk-checkbox__box,
.smbk-checkbox .smbk-checkbox__control[aria-invalid="true"]:enabled:checked + .smbk-checkbox__box,
.smbk-checkbox .smbk-checkbox__control[aria-invalid="true"]:enabled:indeterminate + .smbk-checkbox__box {
border-color: transparent;
box-shadow: 0 0 0 var(--smbk-border-width-emphasis) var(--smbk-color-feedback-danger-border);
}
.smbk-checkbox.smbk-checkbox--error .smbk-checkbox__control:enabled + .smbk-checkbox__box::before,
.smbk-checkbox .smbk-checkbox__control[aria-invalid="true"]:enabled + .smbk-checkbox__box::before {
border-color: transparent;
}
/* Disabled, ticked or not, the box draws itself in the disabled colours, with no fill over it. */
.smbk-checkbox__control:disabled + .smbk-checkbox__box {
background: var(--smbk-color-surface-disabled);
border-color: var(--smbk-color-border-control-disabled);
color: var(--smbk-color-text-disabled);
}
.smbk-checkbox__control:disabled + .smbk-checkbox__box::before {
content: none;
}
/* ---- small (sm) ---- */
.smbk-checkbox--size-sm .smbk-checkbox__box {
width: var(--smbk-size-selection-sm);
height: var(--smbk-size-selection-sm);
border-radius: var(--smbk-radius-control-xs);
}
.smbk-checkbox--size-sm :is(.smbk-checkbox__icon, .smbk-checkbox__mixed-icon) {
font-size: var(--smbk-size-icon-xs);
}
/* ---- motion: ticking fills the box, then draws the tick. The fill grows from a little smaller than the
box as it fades in, with the fill purpose, while the box's own colours and lift fade under it; once the box
has nearly filled, the tick draws itself from its start to its tip with the draw purpose, uncovered
across the box as it fades in. Unticking erases the tick back the same way, without the wait, while the
fill shrinks away. The mixed dash draws without the wait. Only a change moves: a box ticked as the page
loads is simply ticked. Under reduced motion the fill and the tick only fade. ---- */
.smbk-checkbox__box {
transition-property: border-color, background-color, box-shadow;
transition-duration: var(--smbk-motion-fill-duration);
transition-timing-function: var(--smbk-motion-fill-easing);
}
.smbk-checkbox__box::before {
scale: 0.7;
transition-property: opacity, scale;
transition-duration: var(--smbk-motion-fill-duration);
transition-timing-function: var(--smbk-motion-fill-easing);
}
.smbk-checkbox__control:checked + .smbk-checkbox__box::before,
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box::before,
.smbk-checkbox__box--checked::before {
scale: none;
}
.smbk-checkbox__icon,
.smbk-checkbox__mixed-icon {
opacity: 0;
clip-path: inset(0 100% 0 0);
transition-property: visibility, opacity, clip-path;
transition-duration: var(--smbk-motion-draw-duration);
transition-timing-function: var(--smbk-motion-draw-easing);
}
.smbk-checkbox__control:checked:not(:indeterminate) + .smbk-checkbox__box .smbk-checkbox__icon,
.smbk-checkbox__box--checked .smbk-checkbox__icon,
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box .smbk-checkbox__mixed-icon {
opacity: 1;
clip-path: inset(0);
transition-delay: var(--smbk-motion-draw-delay);
}
.smbk-checkbox__control:indeterminate + .smbk-checkbox__box .smbk-checkbox__mixed-icon {
transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
.smbk-checkbox__box::before {
scale: none;
transition-property: opacity;
}
.smbk-checkbox__icon,
.smbk-checkbox__mixed-icon {
clip-path: none;
transition-property: visibility, 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) {
.smbk-checkbox__control:disabled + .smbk-checkbox__box {
border-color: GrayText;
color: 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 | |
| space.inset.2xs--smbk-space-inset-2xs | 2px | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.control--smbk-color-border-control | #7c8692 | Color | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| color.surface.raised--smbk-color-surface-raised | light: #f9fafbdark: #232527changes with theme | Color | |
| color.text.inverse--smbk-color-text-inverse | light: #f9fafbdark: #232527changes with theme | Color | |
| elevation.control--smbk-elevation-control | light: 0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430ddark: 0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a | Elevation | |
| color.border.control-selected--smbk-color-border-control-selected | light: #ffffff1adark: #ffffff80changes with theme | Color | |
| color.surface.inverse--smbk-color-surface-inverse | light: #2d3237dark: #e7ebf0changes with theme | Color | |
| color.border.strong--smbk-color-border-strong | light: #9ba5b1dark: #373d43changes with theme | Color | |
| color.surface.sunken--smbk-color-surface-sunken | light: #373d430ddark: #00000066changes with theme | Color | |
| size.icon.sm--smbk-size-icon-sm | 16px | Size | |
| 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.disabled--smbk-color-surface-disabled | light: #e7ebf0dark: #373d43changes with theme | Color | |
| color.border.control-disabled--smbk-color-border-control-disabled | light: #c4cdd8dark: #606a75changes with theme | Color | |
| radius.control.xs--smbk-radius-control-xs | 2px | Radius | |
| size.icon.xs--smbk-size-icon-xs | 12px | Size | |
| motion.fill.duration--smbk-motion-fill-duration | 320ms | Motion | |
| motion.fill.easing--smbk-motion-fill-easing | cubic-bezier(0.4, 0, 0.2, 1) | Motion | |
| motion.draw.duration--smbk-motion-draw-duration | 320ms | Motion | |
| motion.draw.easing--smbk-motion-draw-easing | cubic-bezier(0, 0, 0.58, 1) | Motion | |
| motion.draw.delay--smbk-motion-draw-delay | 200ms | Motion |
Manifest
{
"name": "checkbox",
"block": "smbk-checkbox",
"summary": "A box that is ticked or not, with its label beside it and an optional line under the label, for choices that are independent of each other.",
"adapt": "css",
"axes": {
"size": {
"values": [
"md",
"sm"
],
"default": "md"
},
"tone": {
"values": [
"default",
"subtle"
],
"default": "default",
"description": "Default is raised with a lift; subtle is sunken with the strong border, for dense places such as table rows."
}
},
"flags": {
"error": {
"description": "The choice is invalid, such as terms that must be accepted. An empty box takes the danger edge; a ticked one keeps its fill inside a danger ring; the control also carries aria-invalid."
}
},
"parts": [
"control",
"box",
"icon",
"mixed-icon",
"label",
"text",
"description"
],
"figmaNodeId": "4068:10770"
}