Checkbox
A native <input type="checkbox"> styled as a small square box. See Radio for the round single-choice variant and Switch for the track-and-thumb.
Basic
Add .checkbox to the input. Wrap the input + label in .field-row so they sit on a flex row with the right gap.
<div class="demo-stack" style="max-width: 24rem;">
<div class="field-row">
<input class="checkbox" type="checkbox" id="defaultCheck" />
<label class="field-row__label" for="defaultCheck">Default checkbox</label>
</div>
<div class="field-row">
<input class="checkbox" type="checkbox" id="checkedCheck" checked />
<label class="field-row__label" for="checkedCheck">Checked by default</label>
</div>
</div>Inline
Add .field-row--inline to line items up on the same row.
<div class="demo-row">
<div class="field-row field-row--inline">
<input class="checkbox" type="checkbox" id="inlineCheck1" />
<label class="field-row__label" for="inlineCheck1">One</label>
</div>
<div class="field-row field-row--inline">
<input class="checkbox" type="checkbox" id="inlineCheck2" />
<label class="field-row__label" for="inlineCheck2">Two</label>
</div>
<div class="field-row field-row--inline">
<input class="checkbox" type="checkbox" id="inlineCheck3" />
<label class="field-row__label" for="inlineCheck3">Three</label>
</div>
</div>Indeterminate
The indeterminate state is set from script. There's no HTML attribute for it. Useful as a parent of a partially-selected group.
<div class="demo-stack" style="max-width: 24rem;">
<div class="field-row">
<input class="checkbox" type="checkbox" id="indeterminateCheck" />
<label class="field-row__label" for="indeterminateCheck">Select all</label>
</div>
<script>
document.getElementById('indeterminateCheck').indeterminate = true;
</script>
</div>Reverse
Add .field-row--reverse to flip the label to the start and the input to the end. Useful for settings rows where the affordance sits on the right edge.
<div class="demo-stack" style="max-width: 24rem;">
<div class="field-row field-row--reverse">
<input class="checkbox" type="checkbox" id="reverseCheck" />
<label class="field-row__label" for="reverseCheck">Reversed checkbox</label>
</div>
</div>Disabled
Add disabled to dim the input and its label, and block interaction.
<div class="demo-stack" style="max-width: 24rem;">
<div class="field-row">
<input class="checkbox" type="checkbox" id="disabledCheck" disabled />
<label class="field-row__label" for="disabledCheck">Disabled checkbox</label>
</div>
<div class="field-row">
<input class="checkbox" type="checkbox" id="disabledCheckedCheck" checked disabled />
<label class="field-row__label" for="disabledCheckedCheck">Disabled, checked</label>
</div>
</div>Browser validation
Pair required with the native :user-invalid pseudo. The browser fires it after the user attempts to submit the form, and clears it the moment the constraint is satisfied. Use for inline validation where the affordance owns its own state.
<form class="demo-stack" style="max-width: 24rem;" onsubmit="event.preventDefault()">
<div class="field-row">
<input class="checkbox" type="checkbox" id="reqTerms" required />
<label class="field-row__label" for="reqTerms">Accept the terms</label>
</div>
<button type="submit" class="btn btn--primary align-self-start">Submit</button>
</form>Hit Submit without checking to trigger :user-invalid. Check the box and the red clears on its own.
Server validation
Set aria-invalid="true" from your form library. The attribute is sticky. Stisla just paints the red while the attribute is present. Remove it when your validator considers the field resolved.
<div class="demo-stack" style="max-width: 24rem;">
<div class="field-row">
<input class="checkbox" type="checkbox" id="srvTerms" aria-invalid="true" />
<label class="field-row__label" for="srvTerms">Accept the terms</label>
</div>
<div class="field-row">
<input class="checkbox" type="checkbox" id="srvTermsChecked" aria-invalid="true" checked />
<label class="field-row__label" for="srvTermsChecked">Accept the terms (checked, server still flagged)</label>
</div>
</div>The red border stays even after you check. That's the point. Server errors shouldn't dismiss themselves on touch. The checked variant shows both signals at once. Primary fill says "this is selected", red rim says "and the server still considers it invalid".
Without labels
Drop the .field-row wrapper to use a bare .checkbox. Always pair with an aria-label for accessibility. Common in tables (row-select) and toolbars.
<div class="demo-row">
<input class="checkbox" type="checkbox" aria-label="Bare checkbox" />
<input class="checkbox" type="checkbox" aria-label="Bare checkbox, checked" checked />
</div>Customization
Six variables retune .checkbox without touching component CSS. Override on the input itself, on a parent scope, or on :root.
| Variable | Default | Use |
|---|---|---|
--checkbox-size |
calc(1rem * var(--st-density)) | Box dimension; density scales it |
--checkbox-radius |
0.25rem | Corner radius; raise to round the corners or zero out for sharp edges |
--checkbox-bg |
var(--st-surface) | Unchecked background |
--checkbox-border |
var(--st-border) | Unchecked border; validation hooks flip this to --st-danger |
--checkbox-checked-bg |
var(--st-primary) | Checked or indeterminate background |
--checkbox-indicator |
SVG data URL (checkmark or dash) | Glyph painted over --checkbox-checked-bg. Checked and indeterminate each set their own SVG. The fill color is a literal because data: URLs can't read CSS variables. To recolor, replace the URL with one whose fill matches the new color |