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

Block: `smbk-checkbox`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `md`, `sm` | `md` |  |
| `tone` | `default`, `subtle` | `default` | Default is raised with a lift; subtle is sunken with the strong border, for dense places such as table rows. |
| `error` | on / 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. |

Parts: `smbk-checkbox__control`, `smbk-checkbox__box`, `smbk-checkbox__icon`, `smbk-checkbox__mixed-icon`, `smbk-checkbox__label`, `smbk-checkbox__text`, `smbk-checkbox__description`

## Markup

```html
<label class="smbk-checkbox smbk-checkbox--size-md smbk-checkbox--tone-default">
  <input type="checkbox" class="smbk-checkbox__control" checked=""/>
  <span class="smbk-checkbox__box" aria-hidden="true">
    <i class="ph ph-check smbk-icon smbk-icon--size-md smbk-checkbox__icon" aria-hidden="true"></i>
    <i
      class="ph ph-minus smbk-icon smbk-icon--size-md smbk-checkbox__mixed-icon"
      aria-hidden="true"
    ></i>
  </span>
  <span class="smbk-checkbox__label">Remember me</span>
</label>
```

## React

```tsx
import { Checkbox } from './design-system/components/checkbox/checkbox.js';

<Checkbox size="md" tone="default" defaultChecked>Remember me</Checkbox>
```

## Stylesheet

```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

```json
{
  "space": {
    "inline": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    },
    "stack": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things stacked one above another, medium."
      }
    },
    "inset": {
      "2xs": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Padding inside a component, smallest."
      }
    }
  },
  "color": {
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "inverse": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "Text on the opposite surface."
      }
    },
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      },
      "control": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "The edge that marks a control: a checkbox, a radio (must reach 3:1)."
      },
      "control-selected": {
        "$type": "color",
        "$value": "{{color.alpha.white.10}}",
        "$description": "A selected control’s edge."
      },
      "strong": {
        "$type": "color",
        "$value": "{{color.grey.400}}",
        "$description": "Stronger edges and separators."
      },
      "control-hover": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "A control’s edge on hover."
      },
      "control-disabled": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "A disabled control’s edge."
      }
    },
    "surface": {
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      },
      "inverse": {
        "$type": "color",
        "$value": "{{color.grey.850}}",
        "$description": "The opposite surface: tooltips, dark chips."
      },
      "sunken": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.5}}",
        "$description": "Recessed wells: fields, tracks, code. See-through, so a well always shows one step below what it sits on: the page, a card or a panel."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Surface of a disabled card or field."
      }
    },
    "action": {
      "inverse": {
        "background-hover": {
          "$type": "color",
          "$value": "{{color.grey.900}}",
          "$description": "Dark chip and radio: fill on hover."
        }
      }
    },
    "feedback": {
      "danger": {
        "border": {
          "$type": "color",
          "$value": "{{color.red.500}}",
          "$description": "Danger messages: edge; the edge of a field in error (3:1)."
        }
      }
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "size": {
    "target": {
      "min": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Smallest tap area allowed (24, WCAG 2.2)."
      },
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    },
    "selection": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Checkbox and radio box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Checkbox and radio box, small."
      }
    },
    "icon": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{size.12}}",
        "$description": "Icon box, extra small."
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    },
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    }
  },
  "radius": {
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{radius.2}}",
        "$description": "Corners of controls, extra small."
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "motion": {
    "fill": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.standard}}"
      }
    },
    "draw": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.ease-out}}"
      },
      "delay": {
        "$type": "duration",
        "$value": "{{duration.200}}",
        "$description": "How long the tick waits for the fill before it starts to draw."
      }
    }
  }
}
```

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

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

## 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](radio.md).
- A setting that takes effect immediately. Use the [switch](switch.md).

## 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

| Key | What it does |
|---|---|
| Tab | Moves focus to the checkbox. |
| Space | Toggles the checkbox. |

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