defuss-shadcn / Forms & Inputs / checkbox
CheckboxATM
A control that toggles between checked and unchecked. Built on native <input type="checkbox"> with CSS-only checkmark.
On this page (10)
§Default
Basic checkbox with label.
§With description
Checkbox with helper text.
§Disabled
disabled keeps the box legible (full-strength edge, muted fill, a softened primary fill when checked) and mutes the label, with the not-allowed cursor over both - no opacity fade.
§Checked by default
§Indeterminate
The mixed state has no HTML attribute - set it with checkbox.indeterminate = true. Pick a state below; a click on the checkbox clears the mixed state (the browser does that) and the picker follows. For what the state is for - a box that summarises a group - see Select all below.
§Invalid
Shows destructive border via aria-invalid='true'.
§Select all
What the mixed state is for: a parent box above a group. Tick some items and it shows the dash, tick them all and it fills, untick everything and it empties. Clicking it while mixed or empty ticks every item; clicking it while full clears them. The parent names its children with aria-controls, which is also all the small script needs to wire them up.
§Group
Multiple checkboxes in a group using <fieldset> and <legend>.
§States
Machine contract - verified against checkbox.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
checked | boolean | true, false | false | Native checked state (panel checkbox ⇄ real click, both directions). |
disabled | boolean | true, false | false | Interaction off (native disabled attribute). |
required | boolean | true, false | false | Post-validation marks the field invalid (:user-invalid). |
invalid | boolean | true, false | false | Manual invalid flag (aria-invalid="true"). |
indeterminate | boolean | true, false | false | Mixed state (the indeterminate property - no HTML attribute; a click clears it). |
§CSS view file
/* -- Checkbox component ----------------------------------------- */@layer components { .checkbox { appearance: none; margin: 0; /* kill the UA's ~4px input margin - breaks flex/grid rhythm */ width: 1.125rem; height: 1.125rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--background); cursor: pointer; flex-shrink: 0; position: relative; transition: background-color 150ms, border-color 150ms; &:checked { background-color: var(--primary); border-color: var(--primary); &::after { content: ''; position: absolute; left: 50%; top: 45%; width: 6px; height: 10px; border: solid var(--primary-foreground); border-width: 0 2px 2px 0; transform: translate(-50%, -50%) rotate(45deg); } } /* Per spec, an indeterminate checkbox ALSO matches :checked - both ::after rules land on the element, so the dash must explicitly cancel the checkmark's border (else the rotated border renders on top of it). */ &:indeterminate { background-color: var(--primary); border-color: var(--primary); &::after { content: ''; position: absolute; left: 50%; top: 50%; width: 10px; height: 2px; border: none; background: var(--primary-foreground); transform: translate(-50%, -50%); } } &:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } /* Disabled = legible but inert: never an opacity fade (0.5 on a light --border edge left a ghost you had to look for). The shape keeps its full-strength edge, the surface turns muted, a chosen state keeps a softened primary fill, and the label goes muted with the no-entry cursor - "a choice that exists but is unavailable right now". */ &:disabled { background-color: var(--muted); /* a mid-grey edge: --border on the muted fill is too faint to read as a box */ border-color: color-mix(in oklch, var(--muted-foreground) 50%, var(--background)); cursor: not-allowed; &:is(:checked, :indeterminate) { background-color: color-mix(in oklch, var(--primary) 45%, var(--background)); border-color: transparent; } } &:is([aria-invalid="true"], :user-invalid:not(form[data-validate="submit"]:not([data-submitted]) *)) { border-color: var(--destructive); } } /* -- Checkbox item layout --------------------------------- */ .checkbox-item { display: flex; align-items: center; gap: 0.5rem; & > label { font-size: 0.875rem; cursor: pointer; /* leading-none (shadcn parity): a 1.6 line-box centers the LINE, not the glyphs - baseline then floats with the font's ascent, so text rides visibly off the box midline (worst with metric-heavy fallbacks). line-height:1 shrinks the box to the glyphs themselves. */ line-height: 1; } } .checkbox-item-block { display: flex; align-items: flex-start; gap: 0.5rem; & > label { font-size: 0.875rem; cursor: pointer; } & > .checkbox { margin-top: 0.125rem; } } /* Gap hit area: inside an item layout the control's click target spans the gap to its label (a transparent ::before, part of the input's own hit box), so clicking the whitespace between control and text toggles too - the label's `for` covers the text itself. Inline-end only: the label always sits after the control (block layouts place it in the second grid column). */ :is(.checkbox-item, .checkbox-item-block) > .checkbox::before { content: ''; position: absolute; inset-block: -0.25rem; inset-inline: -0.25rem calc(-1 * 0.5rem); } /* Disabled auto-styling */ .checkbox-item:has(.checkbox:disabled), .checkbox-item-block:has(.checkbox:disabled) { cursor: not-allowed; /* the text says "unavailable" too - and never offers the hand pointer for a click that cannot land */ & label { color: var(--muted-foreground); cursor: not-allowed; } } /* -- Accessibility ---------------------------------------- */ @media (prefers-reduced-motion: reduce) { .checkbox { transition: none; } } @media (prefers-contrast: more) { .checkbox { border-width: 2px; } .checkbox:disabled { border-color: var(--muted-foreground); } } @media (forced-colors: active) { .checkbox { appearance: auto; } }}Comments, ideas or improvements? Edit this page's source on GitHub