Skip to content
cennge-ui
GitHubv0.3.1, changelog

Checkbox

A box for a yes-or-no choice, with a third, mixed state for a box that stands for a list.

Email me about cennge-ui
Opened, reviewed and merged
New issues and replies
A new version on npm
On the main branch

2 of 4 events, sent to dev@example.com.

  1. Click a box or its label; Space toggles the box in focus.
  2. While only some events are checked, Everything shows a dash and reads as mixed.
  3. Click the mixed box: every event is checked. Click again to clear them all.
Live: the component itself, not a picture of it.

Field marks

Import
import { Checkbox } from "cennge-ui";
Size
2.8 KB gzip for this import, its dependencies aside
Pattern
WAI-ARIA checkbox (Radix Checkbox): a <button role="checkbox"> named by its label
Keyboard
Space
State
Controlled with isSelected and onValueChange, or uncontrolled with defaultSelected
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Mixed
isIndeterminate draws a dash and reads as aria-checked="mixed"
Forms
Submits value, "on" by default, under name while checked
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: form, table

When to use#

A Checkbox is a choice that takes effect when the form around it is sent: agree to the terms, publish with provenance, include these labels. Several boxes in a list are independent of each other, and one more box above them can stand for the whole list.

Similar#

  • Switch turns a setting on or off the moment it flips, with no form to submit.
  • Radio is for one choice out of several; checkboxes allow any number.
  • Table selects rows with this Checkbox; useTableSelection tells the header box when all or some rows are checked.

With a description#

description sits under the label in smaller, muted text and is linked to the box with aria-describedby, so it is read after the label.

npm links the package to the commit and the workflow that built it.
description.tsx
import { Checkbox } from "cennge-ui";

export default function ProvenanceCheckbox() {
  return (
    <Checkbox defaultSelected description="npm links the package to the commit and the workflow that built it.">
      Publish with provenance
    </Checkbox>
  );
}

Some, all or none#

A parent box is checked when all its children are, mixed when some are, and empty when none are. Pass isIndeterminate for the mixed case; a click on a mixed box reports true, so the parent checks everything first.

select-all.tsx
import { useState } from "react";
import { Checkbox } from "cennge-ui";

const labels = ["bug", "documentation", "good first issue"];

export default function LabelFilter() {
  const [checked, setChecked] = useState<string[]>(["bug"]);
  const all = checked.length === labels.length;

  return (
    <div className="flex flex-col gap-3">
      <Checkbox
        isSelected={all}
        isIndeterminate={checked.length > 0 && !all}
        onValueChange={(isSelected) => setChecked(isSelected ? labels : [])}
      >
        All labels
      </Checkbox>
      {labels.map((label) => (
        <Checkbox
          key={label}
          className="pl-7.5"
          isSelected={checked.includes(label)}
          onValueChange={(isSelected) =>
            setChecked((current) => (isSelected ? [...current, label] : current.filter((item) => item !== label)))
          }
        >
          {label}
        </Checkbox>
      ))}
    </div>
  );
}

Required, in a form#

Inside a Form, isRequired stops a submit while the box is empty. The box turns red, the browser's message shows under the label, and focus moves to the box; checking it clears the error. A checked box submits "on" under its name, or your own value.

Nothing submitted yet

in-a-form.tsx
import { useState } from "react";
import { Button, Checkbox, Form } from "cennge-ui";

export default function DeleteRepositoryForm() {
  const [sent, setSent] = useState<string | null>(null);

  return (
    <Form
      onSubmit={(event) => {
        event.preventDefault();
        setSent(String(new FormData(event.currentTarget).get("confirm")));
      }}
      className="flex w-full max-w-96 flex-col gap-4"
    >
      <Checkbox name="confirm" isRequired>
        I understand that payload-cache and its issues will be deleted
      </Checkbox>
      <Button type="submit" color="danger" className="self-start">
        Delete payload-cache
      </Button>
      <p aria-live="polite" className="text-sm text-muted">
        {sent === null ? "Nothing submitted yet" : `Submitted confirm=${sent}`}
      </p>
    </Form>
  );
}

Colours and sizes#

color paints the checked and mixed box; primary follows the accent. Sizes keep the box aligned with the first line of its label.

colors.tsx
import { Checkbox, type CenngeColor } from "cennge-ui";

const colors: CenngeColor[] = ["default", "primary", "secondary", "success", "warning", "danger", "info"];

export default function CheckboxColors() {
  return (
    <div className="flex flex-col gap-5">
      <div className="flex flex-wrap gap-x-6 gap-y-3">
        {colors.map((color) => (
          <Checkbox key={color} color={color} defaultSelected>
            {color[0]!.toUpperCase() + color.slice(1)}
          </Checkbox>
        ))}
      </div>
      <div className="flex flex-wrap items-start gap-x-6 gap-y-3">
        <Checkbox size="sm" defaultSelected>
          Small
        </Checkbox>
        <Checkbox size="md" defaultSelected>
          Medium
        </Checkbox>
        <Checkbox size="lg" defaultSelected>
          Large
        </Checkbox>
        <Checkbox isDisabled defaultSelected>
          Disabled
        </Checkbox>
      </div>
    </div>
  );
}

Accessibility#

The label is a real <label> for the box, so a click on the text toggles it and screen readers name the box by it. Without visible text, pass aria-label. Space toggles; Enter does nothing, as the WAI-ARIA checkbox pattern says. While isInvalid, the box carries aria-invalid and the error replaces the description.

Props#

PropTypeDefault
children
React.ReactNodeLabel next to the box. Without it, pass aria-label.
–
isSelected
boolean
–
defaultSelected
boolean
–
isIndeterminate
booleanShows a dash, e.g. for "select all" when only some rows are selected.
–
onValueChange
(isSelected: boolean) => void
–
isDisabled
boolean
–
isRequired
boolean
–
description
React.ReactNodeSmall text under the label.
–
errorMessage
React.ReactNodeShown instead of description while isInvalid; inside a <Form>, also its errors for name.
–
isInvalid
boolean
–
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
name
string
–
value
string
–

Also takes every attribute of <button>, except color, onChange, value, defaultValue, which mean something else here.