Checkbox

Checkbox lets people pick any number of options. Use it for independent choices and to agree to terms.

  • Beta
  • Accessibility
  • Figma Checkboxes 427:24119
  • packages/ui/src/components/Checkbox

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.

tsx
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

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react

Peer 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

tsx
// 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

tsx
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, required and Space work without extra code.
  • Use onCheckedChange for the new boolean. onChange still fires with the native event if you need it.
  • Give every checkbox a name: label (preferred), a surrounding Field, or aria-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

Unchecked
Checked
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

Parents see completed lessons and quiz scores, not messages.

New
  • sublabel sits 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).
  • description is a second line linked with aria-describedby.
  • badge and action render outside the <label>, so they aren't part of the name and the action is never nested in it.
  • flip moves the control to the trailing edge, for settings lists where the text leads.

Sizes

PartSizeToken or source
Control frame20 × 20--nu-space-5
Box16 × 16, radius 4 (2px inset)--nu-space-4, --nu-radius-4
Unchecked inner square13 × 13, radius 2.6, shows a 1.5px frameComponent value (Figma geometry)
Check mark9.2 × 6.5 (Remix check-line at 13px; Figma 9.06 × 6.59)--nu-checkbox-icon-size
Indeterminate dash8 × 1.5 (CSS bar)--nu-checkbox-dash-width, --nu-checkbox-dash-height
Pointer target24 × 24 (invisible native input, 2px beyond the frame)--nu-space-0-5
Label rowGap 8 to the text, control top-aligned; 4 between label and description; 10 before the action--nu-space-2, -1, -2-5
TextLabel: 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

DefaultUnchecked shows a State/Faded/Base frame (C48); checked fills State/Primary/Base with a white mark.
IndeterminateA white dash on the primary fill, announced as “mixed”. Click it: it becomes checked.
HoverPoint at these. Unchecked turns Sub-300, checked turns blue-700. Hovering the label has the same effect. Only on hover-capable pointers.
FocusKeyboard focus turns the frame primary (checked: blue-800) and adds the primary focus halo, below 3:1 today (C5).
DisabledSoft-200 with no inner square; checked keeps a white mark. Removed from the tab order.
Error (code-only)Figma has no error state. Code turns the unchecked frame State/Error/Base and sets aria-invalid. Needs design confirmation (pending a decision).

Not available in this space

Disabled label (code-only)Figma has no disabled label style. Code uses Text/Disabled-500 (4.23:1, C8).

State tokens

Set on .nu-checkbox. Override them on a wrapper only for a documented exception.

TokenValueUsed for
--nu-checkbox-bgState/Faded/Base (Figma Bg/Soft-200, C48)Unchecked frame
--nu-checkbox-bg-hoverIcon/Sub-600 (Figma Bg/Sub-300, C48)Unchecked, hover
--nu-checkbox-bg-checkedState/Primary/BaseChecked and indeterminate fill
--nu-checkbox-bg-checked-hoverFoundation blue-700 (Figma: remote primary-darker)Checked, hover
--nu-checkbox-bg-focusState/Primary/BaseUnchecked, focus
--nu-checkbox-bg-checked-focusFoundation blue-800 (Figma: remote primary-dark)Checked, focus
--nu-checkbox-bg-invalidState/Error/BaseUnchecked, invalid (code-only)
--nu-checkbox-bg-disabledBg/Soft-200Disabled
--nu-checkbox-markIcon/WhiteCheck mark and dash
--nu-checkbox-focus-shadowbuttons/primary-focusFocus halo (C5)
--nu-checkbox-inner-shadowregular-shadow/x-smallInner 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 CheckboxIndicator draws 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.

  • 12 min · 4 students finished

  • 18 min · 9 students finished

  • 15 min · Draft

  • 20 min · 2 students finished

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.

Be kind, keep answers your own, and credit your sources.

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.

Other teachers in Biology 101 can copy this lesson.

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.

Send me an email when

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

Checkbox props
PropTypeDefaultDescription
labelReactNode—Visible label (Figma Edit Label), rendered as a native <label>, so clicking it toggles the box. Without it, only the 20px control renders.
sublabelReactNode—Secondary text after the label, e.g. “(Optional)”. Part of the accessible name. No default text. Needs label.
descriptionReactNode—Second line under the label, linked with aria-describedby. Needs label.
badgeReactNode—Element after the label, such as a Badge. Outside the <label>, so not part of the name. Needs label.
actionReactNode—Focusable, labelled element under the text, such as a LinkButton. Never nested in the label. Needs label.
flipbooleanfalsePuts the control after the text (trailing edge). Needs label.
indeterminatebooleanfalseShows a dash and sets the native indeterminate property (announced as “mixed”). Visual only: the form value follows checked.
invalidbooleanfalse (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 / defaultCheckedboolean—Native controlled or uncontrolled value.
disabledbooleanfalse (or from Field)Native disabled. Removes the checkbox from the tab order and greys the label.
requiredbooleanfrom FieldNative required.
idstringauto (or from Field)Id of the native input. The label and description ids derive from it.
aria-describedbystring—Merged with the description id and the Field hint and error ids.
classNamestring—Class for the outer element: the label layout when label is set, otherwise the 20px control.
refRef<HTMLInputElement>—Forwarded to the native input.
…restOmit<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

KeyBehaviour
TabMoves focus to the checkbox. Disabled checkboxes are skipped. Each checkbox is its own tab stop.
SpaceToggles 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 propertyFigma valuesCode
ActiveOff · Onchecked / defaultChecked
IndeterminateOff · Onindeterminate
StateDefault · Hover · Focused · DisabledInteractive states and disabled, plus invalid (code-only)
Edit LabelTextlabel
Sublabel · Edit SublabelBoolean · textsublabel
Description · Edit DescriptionOff · On · textdescription
BadgeBooleanbadge (pass a Badge)
Link ButtonBooleanaction (pass a LinkButton)
FlipOff · Onflip

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.

Don't use Checkbox for

  • Choosing exactly one option. Use Radio.
  • A setting that applies immediately, like a light switch. A switch pattern suits it better.
  • A long list of options in limited space. Use a Dropdown with checkbox items.

Do and don't

DoWrite a positive statement. People check the box to get what the label says.
Don'tDon't write negative labels. Checking “Don't email me” makes people stop and decode a double negative.
DoUse indeterminate for a partly selected list. It tells people some, but not all, of the lessons below are selected.
Don'tDon't use indeterminate as a third answer. “Mixed” isn't a value a person can choose, and the form still submits checked or unchecked.
DoUse radios when only one option is allowed. The control tells people the rule before they try.
Don'tDon't use checkboxes for an either-or choice. People expect to pick several; unchecking the other option for them is a surprise.

Common mistakes

  • Setting indeterminate once 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. Use action instead.
  • Giving a checkbox in a Field its own label too, so it is announced twice.
  • Relying on the red frame alone for an error. It only shows while unchecked and has no text.