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.
DefaultResting, unchecked.
HoverPointer over the box; border darkens.
ActivePointer down. Border meets the foreground.
FocusKeyboard focus ring (#9EB7FF, 2 px + 2 px offset).
CheckedCharcoal fill, off-white check mark.
IndeterminateMixed state — some-but-not-all selected.
DisabledInert. Reduced contrast on every part.
Disabled + checkedFaint check on disabled-tint background.
InvalidRed 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.
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.
Box
16 px square, 4 px radius. Carries the border, fill, and focus ring.
Glyph
Off-white check (
#F9F9F9) when checked; horizontal dash whenindeterminate.Label
Inter Tight 16 / Regular. Sentence case. Sits 8 px to the right of the box.
Description
Inter Tight 14 / Regular at 60% foreground. Optional but recommended for non-obvious choices.
Error message
Inter Tight 14 / Regular in
text.dangerwith a small alert glyph. Linked to the input viaaria-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 — passnameand 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>viahtmlFor. No custom ARIA — the platform already does this well. - Description and error messages are linked via
aria-describedbyso screen readers announce them after the label. - Invalid state sets
aria-invalid="true"on the input when theerrorprop is set, so assistive tech announces the failure as well as the message. - Focus ring is the system ring — 2 px
#9EB7FFwith 2 px offset, drawn viapeer-focus-visibleso it only appears for keyboard users. - Group containers. Wrap related checkboxes in a
<fieldset>with a<legend>(visible orsr-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
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | — | Visible text rendered to the right of the box. Linked to the input via htmlFor. |
| description | ReactNode | — | Optional help text under the label. Linked to the input via aria-describedby. |
| error | ReactNode | — | When set, flips the box red, sets aria-invalid, and renders the message under the label with an alert glyph. |
| checked | boolean | "indeterminate" | — | Controlled state. Pass `"indeterminate"` for parent rows that summarise a set of children. |
| defaultChecked | boolean | "indeterminate" | false | Initial state for uncontrolled use. |
| onCheckedChange | (next: boolean) => void | — | Called with the next boolean state when the user toggles. Indeterminate always resolves to a boolean on click. |
| disabled | boolean | false | Inert state. Reduced contrast across the box, label, and description. |
| name | string | — | Native form name. Pair with form action — the value posts with the form on submit. |
| ...rest | InputHTMLAttributes<HTMLInputElement> | — | All standard checkbox attributes pass through to the underlying input (required, form, autoFocus, etc.). |