Components · Forms

Checkbox

A 16-pixel square that flips between three states — unchecked, checked, indeterminate. Use it in forms for boolean choices the user can pick zero or many of, and for parent rows that summarise a set of children.

Documentedby Derek Fidler

Curated highlights from the Lab — no marketing fluff.

Default + label

Overview

The Checkbox is the canonical form input for zero-or-more selections. Reach for it whenever the user can pick any combination of options independently. For exactly one selection in a small known set, use a Radio group instead. For binary settings that take effect immediately (notifications on / off), reach for a Switch.

Three states, not two

Beyond checked and unchecked, the Checkbox supports indeterminate — a parent row that summarises a set of children where some but not all are on. Pass checked="indeterminate" and the box renders a horizontal dash; clicking it usually toggles all children at once.

States

Nine visible states. Hover, active, and focus are pseudo-class driven — they come for free on the real component. Held still here for review.

VisualStateNotes
  • Default

    Resting, unchecked.

  • Hover

    Pointer over the box; border darkens.

  • Active

    Pointer down. Border meets the foreground.

  • Focus

    Keyboard focus ring (#9EB7FF, 2 px + 2 px offset).

  • Checked

    Charcoal fill, off-white check mark.

  • Indeterminate

    Mixed state — some-but-not-all selected.

  • Disabled

    Inert. Reduced contrast on every part.

  • Disabled + checked

    Faint check on disabled-tint background.

  • Invalid

    Red border. Pair with an error message below.

Compositions

Four canonical compositions cover almost every form. Reach for the one that fits the surface — don't invent a fifth.

Label only

The minimum. Use this in dense forms or when the label is self-explanatory.

Curated highlights from the Lab — no marketing fluff, unsubscribe in one click.

Label + help text

The default. Help text adds context without burying it in a tooltip.

Required to continue.

Please confirm before proceeding.

Invalid + error message

Set the error prop to flip the box red and announce the message via aria-describedby.

Long label

Multi-line labels wrap naturally. The box stays aligned to the first line of text.

Groups

Wrap related checkboxes in a <fieldset> with a <legend> (visible or visually hidden) so screen readers announce the group's purpose. A parent row with indeterminate can summarise the children — toggling it flips all of them.

Email digest sections

Toggle every section in the digest.

One-line totals at end-of-day.

Settlement notifications when funds arrive.

Per-transaction events above your threshold.

Indeterminate is for summaries, not user input

The user never setsa checkbox to indeterminate. It's a derived state — “some of the children below are on.” Clicking it should choose a definite outcome (usually: select all on first click, deselect on the next).

Anatomy

Five named parts. Only the box is required — label, description, and error message are opt-in.

Curated highlights from the Lab.

  1. Box

    16 px square, 4 px radius. Carries the border, fill, and focus ring.

  2. Glyph

    Off-white check (#F9F9F9) when checked; horizontal dash when indeterminate.

  3. Label

    Inter Tight 16 / Regular. Sentence case. Sits 8 px to the right of the box.

  4. Description

    Inter Tight 14 / Regular at 60% foreground. Optional but recommended for non-obvious choices.

  5. Error message

    Inter Tight 14 / Regular in text.danger with a small alert glyph. Linked to the input via aria-describedby.

Behavior

  • Click target is the whole row. Clicking the label toggles the box — the native <label htmlFor> association handles it. Don't intercept clicks on the label.
  • Keyboard activates on Space. Native button-input convention — Space toggles, Tab moves focus. Don't hijack with custom handlers.
  • Indeterminate clears on toggle. When the user clicks an indeterminate parent, it commits to a definite state (true or false) — never returns to indeterminate from a click. The component handles this for uncontrolled use.
  • Form submission is native. The component renders a real <input type="checkbox"> under the hood — pass name and the value posts with the form, no extra wiring needed.
  • Transitions are colour-only. The border and background tweak in 150 ms — no scale, no ripple, no glow. The same restraint as the Button.

Accessibility

  • Native semantics. The component renders an <input type="checkbox"> inside a <label> via htmlFor. No custom ARIA — the platform already does this well.
  • Description and error messages are linked via aria-describedby so screen readers announce them after the label.
  • Invalid state sets aria-invalid="true" on the input when the error prop is set, so assistive tech announces the failure as well as the message.
  • Focus ring is the system ring — 2 px #9EB7FF with 2 px offset, drawn via peer-focus-visible so it only appears for keyboard users.
  • Group containers. Wrap related checkboxes in a <fieldset> with a <legend> (visible or sr-only) so the group's purpose is announced before the first item.

Code

Import from @flatpay-dk/ui. Renders a real native input — every standard checkbox attribute passes through (name, required, form, etc.).

tsx

import { Checkbox } from "@flatpay-dk/ui";

// Default — uncontrolled, label only
<Checkbox label="Send me product updates" />

// Label + help text
<Checkbox
  label="Subscribe to weekly digest"
  description="Curated highlights — no marketing fluff."
/>

// Controlled
<Checkbox
  checked={notifications}
  onCheckedChange={setNotifications}
  label="Notifications enabled"
/>

// Indeterminate (parent row)
<Checkbox
  checked={allOn ? true : someOn ? "indeterminate" : false}
  onCheckedChange={setAll}
  label="All sections"
/>

// Invalid + error message
<Checkbox
  label="I agree to the merchant terms"
  description="Required to continue."
  error="Please confirm before proceeding."
/>

// In a form — native submission, no extra wiring
<form action={submit}>
  <Checkbox name="marketing_opt_in" label="Marketing emails" />
  <Button type="submit">Continue</Button>
</form>

Best practices

The Checkbox is everywhere. A few habits keep it predictable.

Do

Use a checkbox when the user can choose any combination, including none.

Don't

Don't use checkboxes for exactly-one selections — that's a Radio group.

Please confirm before proceeding.

Do

Set the error prop, not a sibling paragraph. The component links it via aria-describedby.

(Required) Please confirm to continue.

Don't

Don't lean on color alone. The error prop adds an alert glyph and the message — both signals.

Props

PropTypeDefaultDescription
labelReactNodeVisible text rendered to the right of the box. Linked to the input via htmlFor.
descriptionReactNodeOptional help text under the label. Linked to the input via aria-describedby.
errorReactNodeWhen set, flips the box red, sets aria-invalid, and renders the message under the label with an alert glyph.
checkedboolean | "indeterminate"Controlled state. Pass `"indeterminate"` for parent rows that summarise a set of children.
defaultCheckedboolean | "indeterminate"falseInitial state for uncontrolled use.
onCheckedChange(next: boolean) => voidCalled with the next boolean state when the user toggles. Indeterminate always resolves to a boolean on click.
disabledbooleanfalseInert state. Reduced contrast across the box, label, and description.
namestringNative form name. Pair with form action — the value posts with the form on submit.
...restInputHTMLAttributes<HTMLInputElement>All standard checkbox attributes pass through to the underlying input (required, form, autoFocus, etc.).