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
| State | Box | Label | Transition |
|---|---|---|---|
| Default | Paper fill, warm border | ink-body | — |
| Hover | Teal-pale fill, teal border | Unchanged | 140ms ease-out |
| Checked | Teal fill + white checkmark | ink | 140ms ease-out |
| Pressed | scale(0.97) | Unchanged | 140ms ease-out |
| Focused | 2px teal outline, 2px offset | Unchanged | Instant |
| Indeterminate | Teal fill + white dash | ink-body | 140ms ease-out |
| Disabled | Muted fill, 50% opacity | 40% 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
| Part | Element | Notes |
|---|---|---|
| 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-box | 20×20px, 6px radius. Visual representation of the input. Transitions background and border. |
| Checkmark | SVG path inside .checkbox-box | Display none by default; display: block when input is checked. White stroke, 1.8px width. |
| Indeterminate dash | .checkbox-indeterminate | Horizontal 10×2px white bar. Shown when input.indeterminate = true. |
| Label | .checkbox-label | 14px body font. Can be omitted for icon-only checkboxes (rare). |
Usage
aria-label.
<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.
<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
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.
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.
<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
| Class | Purpose |
|---|---|
.checkbox-wrap | Label container — flex, pointer, user-select: none |
.checkbox-box | Visual 20×20 square — transitions background and border |
.checkbox-check | SVG checkmark — shown when input is :checked |
.checkbox-indeterminate | White dash — shown when input.indeterminate = true |
.checkbox-label | Text label — transitions color on check |
Design tokens used
| Token | Value | Role |
|---|---|---|
--r-xs | 6px | Box border radius |
--s-paper | #FBF8F0 | Unchecked box background |
--ink-line | #DDD2B8 | Unchecked box border |
--teal | #0E7C66 | Checked fill and border |
--teal-deep | #0A5F4F | Checked hover fill |
--teal-pale | #D6EAE2 | Unchecked hover fill |
--d-fast | 140ms | All transitions |