affectUI › Components › Inputs

Checkbox

A real checkbox with a label and a second line, a some-of-them state for Select all, an error and a card row – the check draws itself.

Free and open source (MIT). Install: npx shadcn@latest add https://www.affectui.com/r/checkbox.json · Import from @/components/ui/checkbox

When to use

  • Yes or no choices that are saved with a form: agreeing to terms, picking several items of a list, opting in
  • Select all over a list – the some-of-them state shows a dash while only part of it is picked

When not to use

  • A setting that applies the moment you flip it – use the Switch
  • One choice out of several – use radio buttons, the Segmented Control or the Select

Usage

import { useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";

export function Topics() {
  const all = ["mentions", "approvals", "summary"];
  const [picked, setPicked] = useState<string[]>([]);

  return (
    <fieldset>
      <legend>Email me about</legend>
      <Checkbox
        label="Everything"
        checked={picked.length === all.length}
        indeterminate={picked.length > 0 && picked.length < all.length}
        onChange={() => setPicked(picked.length === all.length ? [] : all)}
      />
      {all.map((id) => (
        <Checkbox
          key={id}
          label={id}
          checked={picked.includes(id)}
          onChange={(e) => setPicked((p) => (e.target.checked ? [...p, id] : p.filter((x) => x !== id)))}
        />
      ))}
    </fieldset>
  );
}

Props

NameTypeDefaultDescription
labelReactNodeShown next to the box; clicking it toggles
descriptionReactNodeA quiet second line under the label, linked with aria-describedby
indeterminatebooleanfalseSome of them – a dash instead of the check; set it on a Select all whose list is partly picked
errorReactNodeMarks it invalid (red edge, aria-invalid) and shows the message under it
variant"plain" | "card""plain"card: a bordered row that is clickable as a whole; checked, its edge turns dark
...input propsComponentProps<"input">checked, defaultChecked, onChange, name, value, required, disabled, ref – everything goes to the real checkbox

Keyboard

  • Space: Toggles it
  • Tab: Moves to the next checkbox – each one is its own stop, as with native checkboxes

Accessibility

  • A real <input type="checkbox"> inside its <label>: forms, screen readers and the some-of-them state (mixed) work as with any checkbox
  • The description and the error are linked with aria-describedby; an error sets aria-invalid and is announced politely
  • Group related ones in a <fieldset> with a <legend>, so the question is read with each choice
  • The ring shows only under keyboard focus (:focus-visible), never after a click

Responsive

  • Long labels wrap; the box stays on the first line
  • Touch screens get a 20px box, 15px text and a hit area larger than the box
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Switch – On or off for things that apply right away – label and second line, at the start or across a settings row, a thumb that stretches as you press it
  • Select – Pick one option in the Text Field's look – icons and a second line per option, a gliding highlight, type-ahead and a list that never gets cut off
  • Text Field – One input for every form – label, hint and error in place, prefix and suffix, clear, show-password and copy buttons, a character count
  • Segmented Control – Switch between two to six options – a raised pill or underline that glides, stretches a little and can be dragged across, with icons, counts and badges