Checkbox

A custom checkbox with a teal checkmark. Used for multi-select — choosing which members split an expense, selecting expenses to settle in bulk, or enabling optional settings. 20px square with 6px radius to feel considered without being heavy. Scales down 3% on press.

Variants

Unchecked — paper fill, hairline warm border. Waiting for input.

Checked — teal fill, white SVG checkmark. Teal is the settled/confirmed color — checking means "this person is in."

Indeterminate — teal fill, white dash. Appears on a "select all" checkbox when some but not all children are checked. Set via JS: el.indeterminate = true.

Disabled — muted fill, reduced opacity on both box and label. Never interactive. Use sparingly.

States

Default
Hover
Checked
Pressed
Focused
Disabled
StateBoxLabelTransition
DefaultPaper fill, warm borderink-body
HoverTeal-pale fill, teal borderUnchanged140ms ease-out
CheckedTeal fill + white checkmarkink140ms ease-out
Pressedscale(0.97)Unchanged140ms ease-out
Focused2px teal outline, 2px offsetUnchangedInstant
IndeterminateTeal fill + white dashink-body140ms ease-out
DisabledMuted fill, 50% opacity40% opacity

On dark surfaces

On dark surfaces, the unchecked box uses --dark-elevated fill with --dark-border. Checked state is unchanged — teal fill reads well on dark. Label text uses --dark-text-2 (unchecked) and --dark-text (checked). Add .on-dark to the parent.

Anatomy

Include Kabir
Box (20×20, 6px radius)
Label
Checkmark (SVG)
PartElementNotes
Wrapper<label class="checkbox-wrap">Makes the entire label area clickable. Cursor: pointer. Contains the native input (hidden), box, and label.
Native input<input type="checkbox">Opacity 0, size 0. Still receives focus and keyboard events. Do not display:none — that removes it from tab order.
Box.checkbox-box20×20px, 6px radius. Visual representation of the input. Transitions background and border.
CheckmarkSVG path inside .checkbox-boxDisplay none by default; display: block when input is checked. White stroke, 1.8px width.
Indeterminate dash.checkbox-indeterminateHorizontal 10×2px white bar. Shown when input.indeterminate = true.
Label.checkbox-label14px body font. Can be omitted for icon-only checkboxes (rare).

Usage

Do Use checkboxes for multi-select — when more than one option can be selected simultaneously. Pair "select all" with the indeterminate state. Labels should be sentence case, concise noun phrases (person name, setting name).
Don't Don't use a checkbox for a single binary toggle — use Toggle for that. Don't use checkboxes in place of radio buttons when only one option can be active. Don't hide the label without providing an aria-label.
Do Always use a <label> wrapper (or for attribute) so the click target includes the label text. 14px text + 10px gap is the minimum comfortable touch target alongside the 20px box.
Don't Don't style the native <input> directly. Keep the native element hidden but accessible. Don't set display:none or visibility:hidden on the native input — both remove it from keyboard navigation.

Accessibility

Keyboard Checkboxes are focusable via Tab. Toggle with Space. The native input handles all keyboard behavior — never prevent default or intercept Space. The focus ring (:focus-visible) appears on keyboard focus, not mouse click.
Indeterminate state The indeterminate state is set via JavaScript (el.indeterminate = true), not via an HTML attribute. Screen readers announce it as "mixed" for role="checkbox". The "select all" checkbox should have aria-label="Select all expenses" rather than relying on adjacent text.
Groups When multiple checkboxes form a related group (e.g. "who splits this expense"), wrap them in a <fieldset> with a <legend>. This gives screen readers the group context without requiring each checkbox label to repeat the group name.

Code

HTML

<!-- Unchecked -->
<label class="checkbox-wrap">
  <input type="checkbox">
  <span class="checkbox-box">
    <svg class="checkbox-check" viewBox="0 0 12 12" fill="none">
      <path d="M2 6L5 9L10 3" stroke="white" stroke-width="1.8"
            stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    <span class="checkbox-indeterminate"></span>
  </span>
  <span class="checkbox-label">Priya</span>
</label>

<!-- Checked -->
<label class="checkbox-wrap">
  <input type="checkbox" checked>
  <!-- same box markup -->
</label>

// Indeterminate (JS only)
document.getElementById("select-all").indeterminate = true;

CSS classes

ClassPurpose
.checkbox-wrapLabel container — flex, pointer, user-select: none
.checkbox-boxVisual 20×20 square — transitions background and border
.checkbox-checkSVG checkmark — shown when input is :checked
.checkbox-indeterminateWhite dash — shown when input.indeterminate = true
.checkbox-labelText label — transitions color on check

Design tokens used

TokenValueRole
--r-xs6pxBox border radius
--s-paper#FBF8F0Unchecked box background
--ink-line#DDD2B8Unchecked box border
--teal#0E7C66Checked fill and border
--teal-deep#0A5F4FChecked hover fill
--teal-pale#D6EAE2Unchecked hover fill
--d-fast140msAll transitions