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