Checkbox
Checkbox lets people pick any number of options. Use it for independent choices and to agree to terms.
Beta until the focus indicator (C5) is resolved, and design confirms the code-only error and disabled-label states. Everything else on this page is implemented and tested.
Preview
Change the controls to try every supported combination. The preview is a real, controlled Checkbox: click it and the value control follows, and the code updates with it.
We send one summary a day, not one email per student.
import { Checkbox } from '@nuton/ui'
const [checked, setChecked] = useState(true)
<Checkbox
label="Email me when a student finishes a lesson"
description="We send one summary a day, not one email per student."
checked={checked}
onCheckedChange={setChecked}
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactPeer dependencies: react and react-dom 18 or later. @remixicon/react draws the check mark (Remix Icon is Nuton's icon set, D1).
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)Labels use --nu-font-body (Inter). Map the token to your loaded font as described on the Button page. Checkbox is a client component ('use client'), so it works in server-rendered pages without extra setup.
Usage
import { useState } from 'react'
import { Checkbox } from '@nuton/ui'
export function LessonReminders() {
const [remind, setRemind] = useState(true)
return (
<Checkbox
label="Remind students before a lesson is due"
checked={remind}
onCheckedChange={setRemind}
/>
)
}- Checkbox renders a native
<input type="checkbox">, so forms,name/value,requiredand Space work without extra code. - Use
onCheckedChangefor the newboolean.onChangestill fires with the native event if you need it. - Give every checkbox a name:
label(preferred), a surrounding Field, oraria-label. A development warning flags a missing name. - Write labels as a positive statement in sentence case: “Email me a weekly summary”, not “Don't email me”.
- Each checkbox is independent. When only one option may be chosen, use Radio.
Variants and sizes
Checkbox has one size. Its variations are the value (unchecked, checked or indeterminate) and the optional text around the control, which follows Figma's Checkbox Label.
Value
<Checkbox aria-label="Unchecked" />
<Checkbox aria-label="Checked" defaultChecked />
<Checkbox aria-label="Indeterminate" indeterminate />indeterminate is visual: it sets the native indeterminate property, so assistive technology announces “mixed”, but the submitted value still follows checked. Clicking clears it, so control it from state (see Select all lessons).
Label options
<Checkbox label="Show answers after the quiz" />
<Checkbox label="Allow late submissions" sublabel="(Optional)" />
<Checkbox
label="Share progress with parents"
description="Parents see completed lessons and quiz scores, not messages."
/>
<Checkbox
label="Generate practice questions"
badge={<Badge size="sm" color="blue" appearance="light">New</Badge>}
action={<LinkButton href="/help/practice-questions" tone="primary" size="sm">How it works</LinkButton>}
/>
<Checkbox label="Notify me about new students" flip />sublabelsits inside the<label>, so it is part of the accessible name. It has no default text: pass a translated string. It is spaced with a margin, not a space character, so some browsers may join it to the label in the computed name (pending a decision).descriptionis a second line linked witharia-describedby.badgeandactionrender outside the<label>, so they aren't part of the name and the action is never nested in it.flipmoves the control to the trailing edge, for settings lists where the text leads.
Sizes
| Part | Size | Token or source |
|---|---|---|
| Control frame | 20 × 20 | --nu-space-5 |
| Box | 16 × 16, radius 4 (2px inset) | --nu-space-4, --nu-radius-4 |
| Unchecked inner square | 13 × 13, radius 2.6, shows a 1.5px frame | Component value (Figma geometry) |
| Check mark | 9.2 × 6.5 (Remix check-line at 13px; Figma 9.06 × 6.59) | --nu-checkbox-icon-size |
| Indeterminate dash | 8 × 1.5 (CSS bar) | --nu-checkbox-dash-width, --nu-checkbox-dash-height |
| Pointer target | 24 × 24 (invisible native input, 2px beyond the frame) | --nu-space-0-5 |
| Label row | Gap 8 to the text, control top-aligned; 4 between label and description; 10 before the action | --nu-space-2, -1, -2-5 |
| Text | Label: Label/Small 14/20, weight 500. Sublabel and description: Paragraph/X Small 12/16 | --nu-type-label-small-*, --nu-type-paragraph-x-small-* |
Values from the component CSS, which follows Figma's geometry (Checkbox [1.1] 427:24777, Checkbox Label [1.1] 427:24813). Figma has one size only.
States and interactions
aria-invalid. Needs design confirmation (pending a decision).Not available in this space
State tokens
Set on .nu-checkbox. Override them on a wrapper only for a documented exception.
| Token | Value | Used for |
|---|---|---|
--nu-checkbox-bg | State/Faded/Base (Figma Bg/Soft-200, C48) | Unchecked frame |
--nu-checkbox-bg-hover | Icon/Sub-600 (Figma Bg/Sub-300, C48) | Unchecked, hover |
--nu-checkbox-bg-checked | State/Primary/Base | Checked and indeterminate fill |
--nu-checkbox-bg-checked-hover | Foundation blue-700 (Figma: remote primary-darker) | Checked, hover |
--nu-checkbox-bg-focus | State/Primary/Base | Unchecked, focus |
--nu-checkbox-bg-checked-focus | Foundation blue-800 (Figma: remote primary-dark) | Checked, focus |
--nu-checkbox-bg-invalid | State/Error/Base | Unchecked, invalid (code-only) |
--nu-checkbox-bg-disabled | Bg/Soft-200 | Disabled |
--nu-checkbox-mark | Icon/White | Check mark and dash |
--nu-checkbox-focus-shadow | buttons/primary-focus | Focus halo (C5) |
--nu-checkbox-inner-shadow | regular-shadow/x-small | Inner square shadow (nearest token) |
Figma discrepancies (pending a decision)
- Checkbox hover and focus colours differ from Radio's: Checkbox checked-hover is blue-700 and checked-focus blue-800; Radio uses blue-800 and blue-900. Code follows each spec with local tokens in place of Figma's remote ones. Proposal: align both.
- The inner square's shadow (0 2 2 0, 12%) has no matching token; the nearest,
regular-shadow/x-small, is used, as in Menu. - Menu's
CheckboxIndicatordraws a 12px check and a Remix dash; Checkbox uses a 13px check and an 8 × 1.5 CSS bar, closer to Figma.
Composition examples
Select all lessons
A parent checkbox summarises a list. It is checked when every lesson is selected, unchecked when none is, and indeterminate in between. Checking it selects all; unchecking it clears all. Try it, then add the selection to a space.
Required agreement
Validate on submit, not on load. When the box is still unchecked, set invalid, put the reason in the description (it is linked with aria-describedby) and move focus to the checkbox.
The description keeps the Sub-600 colour when invalid; only the frame turns red. Use the error colour for the message by wrapping the control in a Field instead (below).
Inside a Field
A Field names a single checkbox with its label and links its hint and error. Leave out the checkbox's own label, or it would be named twice.
Groups of checkboxes
There is no checkbox group component. For a set of related options, wrap them in a <fieldset> with a <legend>, or an element with role="group" and aria-labelledby, so the question is announced with each option.
Gap: Checkbox Card isn't built (pending a decision)
Figma has a Checkbox Card (427:24878, 6 types × 4 states). It is outside the current API, and some types show brand or card-provider assets, which C15 and D12 rule out. Use a Checkbox with description until a card is designed for Nuton.
API reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Visible label (Figma Edit Label), rendered as a native <label>, so clicking it toggles the box. Without it, only the 20px control renders. |
sublabel | ReactNode | — | Secondary text after the label, e.g. “(Optional)”. Part of the accessible name. No default text. Needs label. |
description | ReactNode | — | Second line under the label, linked with aria-describedby. Needs label. |
badge | ReactNode | — | Element after the label, such as a Badge. Outside the <label>, so not part of the name. Needs label. |
action | ReactNode | — | Focusable, labelled element under the text, such as a LinkButton. Never nested in the label. Needs label. |
flip | boolean | false | Puts the control after the text (trailing edge). Needs label. |
indeterminate | boolean | false | Shows a dash and sets the native indeterminate property (announced as “mixed”). Visual only: the form value follows checked. |
invalid | boolean | false (or from Field) | Sets aria-invalid and the error frame while unchecked. Inside a Field, it comes from error. Code-only state. |
onCheckedChange | (checked: boolean) => void | — | Called with the new checked state. onChange still fires with the native event. |
checked / defaultChecked | boolean | — | Native controlled or uncontrolled value. |
disabled | boolean | false (or from Field) | Native disabled. Removes the checkbox from the tab order and greys the label. |
required | boolean | from Field | Native required. |
id | string | auto (or from Field) | Id of the native input. The label and description ids derive from it. |
aria-describedby | string | — | Merged with the description id and the Field hint and error ids. |
className | string | — | Class for the outer element: the label layout when label is set, otherwise the 20px control. |
ref | Ref<HTMLInputElement> | — | Forwarded to the native input. |
…rest | Omit<InputHTMLAttributes, 'type' | 'size' | 'children'> | — | Any other native input attribute (name, value, onChange, aria-label, form…), spread on the input. |
Styling hooks
The control renders .nu-checkbox (with data-invalid and data-disabled) around .nu-checkbox__input and .nu-checkbox__box. With a label, the layout is .nu-choice with data-kind="checkbox", data-flip, data-disabled and data-has-description, shared with Radio. Use props rather than styling these; the --nu-checkbox-* tokens (see State tokens) are the supported override points.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the checkbox. Disabled checkboxes are skipped. Each checkbox is its own tab stop. |
| Space | Toggles the checkbox (native behaviour). An indeterminate checkbox becomes checked. |
Screen readers
Native role checkbox, announced as checked, not checked or mixed. The name comes from label plus sublabel, a Field label or aria-label; the description and Field hint and error are read as the description. The box and mark are hidden.
Focus
The frame turns primary and the primary focus halo is added. The halo measures about 1.1:1 against white, below 3:1 (C5).
Contrast and colour
The unchecked frame is State/Faded/Base, 4.23:1 against white, and its hover Icon/Sub-600, 6.69:1, both above WCAG 1.4.11's 3:1 (C48; Figma's Soft-200 was 1.19:1). Checked focus is 1.64:1 against the checked fill. The error frame is 4.15:1 (C6). Disabled marks and labels are 1.19:1 and 1.53:1 (C8). The label is 17.93:1 and the description 6.69:1.
Touch and pointer
The invisible native input is 24 × 24, meeting WCAG 2.5.8 without a label. With a label, the whole label is a target too. Hover styles apply only on hover-capable pointers.
You must: give every checkbox a name (label, a Field or aria-label); control indeterminate from state; say why an unchecked required checkbox is invalid in text, not only with the red frame; group related checkboxes under a legend or a labelled group.
Figma mapping
| Figma property | Figma values | Code |
|---|---|---|
| Active | Off · On | checked / defaultChecked |
| Indeterminate | Off · On | indeterminate |
| State | Default · Hover · Focused · Disabled | Interactive states and disabled, plus invalid (code-only) |
| Edit Label | Text | label |
| Sublabel · Edit Sublabel | Boolean · text | sublabel |
| Description · Edit Description | Off · On · text | description |
| Badge | Boolean | badge (pass a Badge) |
| Link Button | Boolean | action (pass a LinkButton) |
| Flip | Off · On | flip |
Guidelines and related components
Use Checkbox for
- Choosing any number of options from a list, including none.
- A single opt-in or agreement, such as accepting space guidelines.
- Selecting items in a list for a bulk action, with a “select all” parent.
Do and don't
Common mistakes
- Setting
indeterminateonce and never updating it. A click clears the native property; drive it from state on every render. - Putting a link inside
label. Clicking it toggles the box. Useactioninstead. - Giving a checkbox in a Field its own
labeltoo, so it is announced twice. - Relying on the red frame alone for an error. It only shows while unchecked and has no text.