Menu


Checkbox


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.

Customize the preview


Choices
Flags
View

Options


Checkbox options
ChoiceValuesDefaultWhat it does
sizesmbk-checkbox--size-<value>mdsmmd
tonesmbk-checkbox--tone-<value>defaultsubtledefault
Default is raised with a lift; subtle is sunken with the strong border, for dense places such as table rows.
errorsmbk-checkbox--errorOn or offOff
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.

Partssmbk-checkbox__controlsmbk-checkbox__boxsmbk-checkbox__iconsmbk-checkbox__mixed-iconsmbk-checkbox__labelsmbk-checkbox__textsmbk-checkbox__description

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

  • One choice from several. Use radios.
  • A setting that takes effect immediately. Use the switch.

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.
Do

Name the thing being chosen, so a tick clearly means yes to it.

Don't

A question as the label leaves it unclear whether a tick means yes or no.

Notifications
Do

Stack related checkboxes under one group label, so they read as one list.

Don't

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 indeterminate sets the control's indeterminate property 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-label instead.
  • In plain markup, set the mixed state by script, since no attribute sets it: it is the control's indeterminate property. 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

Checkbox keys
KeyWhat 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
checkbox.css
/* 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
Checkbox 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
space.inset.2xs--smbk-space-inset-2xs2pxSpacing
border-width.hairline--smbk-border-width-hairline1pxBorder width
color.border.control--smbk-color-border-control#7c8692Color
radius.control.sm--smbk-radius-control-sm4pxRadius
color.surface.raised--smbk-color-surface-raisedlight: #f9fafbdark: #232527changes with themeColor
color.text.inverse--smbk-color-text-inverselight: #f9fafbdark: #232527changes with themeColor
elevation.control--smbk-elevation-controllight: 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 #0000001aElevation
color.border.control-selected--smbk-color-border-control-selectedlight: #ffffff1adark: #ffffff80changes with themeColor
color.surface.inverse--smbk-color-surface-inverselight: #2d3237dark: #e7ebf0changes with themeColor
color.border.strong--smbk-color-border-stronglight: #9ba5b1dark: #373d43changes with themeColor
color.surface.sunken--smbk-color-surface-sunkenlight: #373d430ddark: #00000066changes with themeColor
size.icon.sm--smbk-size-icon-sm16pxSize
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.disabled--smbk-color-surface-disabledlight: #e7ebf0dark: #373d43changes with themeColor
color.border.control-disabled--smbk-color-border-control-disabledlight: #c4cdd8dark: #606a75changes with themeColor
radius.control.xs--smbk-radius-control-xs2pxRadius
size.icon.xs--smbk-size-icon-xs12pxSize
motion.fill.duration--smbk-motion-fill-duration320msMotion
motion.fill.easing--smbk-motion-fill-easingcubic-bezier(0.4, 0, 0.2, 1)Motion
motion.draw.duration--smbk-motion-draw-duration320msMotion
motion.draw.easing--smbk-motion-draw-easingcubic-bezier(0, 0, 0.58, 1)Motion
motion.draw.delay--smbk-motion-draw-delay200msMotion
Manifest
checkbox.manifest.json
{
  "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"
}