Checkbox

Developer Experience (DX)

How developers use the Checkbox, with one consistent, typed API that works with your forms however you build them.

Next, let's look at how developers use the Checkbox. It has one API, with the same names as every other component, it's typed end to end, and it works with your forms however you build them.

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

export function Updates() {
  const [updates, setUpdates] = useState(false);

  return (
    <Checkbox checked={updates} onChange={(event) => setUpdates(event.target.checked)}>
      Email me product updates
    </Checkbox>
  );
}

Consistent

Once you've learned the Button's and the Input's APIs, you already know most of the Checkbox's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropxs, sm, md, lg or xl, with the same text everywhere
disabledPropThe component can't be used
readonlyPropIt can be focused, and its form sends it, but it can't be changed
checkedPropWhether it's checked, which you bind the way your framework binds a checkbox's
indeterminatePropNeither checked nor unchecked, as the native property means it
Default slotSlotThe label, as it is for the Button

We recommend size="md" as the default. The Checkbox also leaves a few props out, on purpose:

  • There's no label prop. The label is the content between its tags, which can hold more than plain text, like a word in bold, and which the <label> wraps either way.
  • There's no color or variant. A checked box has no intent of its own, and its one other coloured state, invalid, comes from aria-invalid, so its colour and its state can't disagree. To match your brand, set --checkbox--box--fill from a class, as under Tokens.
  • There's no hint or error. Each of them is an element of its own, linked by ID, as on the Input, so the Checkbox fits any layout.
  • There's no options or array value. A list of options is a group, which the Checkbox Group builds out of Checkboxes, with a <fieldset> and a <legend>.

Type-safe

Props, events and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like size="medium", which isn't one of the sizes.

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"How big the box and its label are
checkedbooleanfalseWhether it's checked, when you bind it. Without it, the box keeps its own
indeterminatebooleanfalseShows a dash, and makes screen readers announce it as mixed
disabledbooleanfalseBlocks focus, toggling and submission
readonlybooleanfalseBlocks toggling, while it stays focusable and its form sends it
EventPayloadDescription
changeEventFires when people check or uncheck it, with a click, a tap or Space

It comes straight from the <input>, like every other native event, such as focus and blur, and a binding to checked listens to it for you. Read the new state from event.target.checked.

SlotDescription
DefaultThe label, which names the checkbox, and which a press on toggles the box, unless it's read-only

We also export CheckboxProps, which comes in handy for wrappers and data-driven forms:

import type { CheckboxProps } from "@/components/Checkbox";

const preferences: (CheckboxProps & { name: string; label: string })[] = [
  { name: "updates", label: "Email me product updates" },
  { name: "texts", label: "Text me about my orders", disabled: true },
];

Composable

The Checkbox renders a <label> around the <input>, so it splits what you pass between them. Classes and styles go on the <label>, so a class can set its variables. Everything else lands on the <input>, including id, name, value, required, ARIA attributes and event listeners. A ref to the Checkbox reaches the <input> too, since that's what form libraries and focus() need.

Forms

The <input> is a real form control, so the Checkbox works with your forms however you build them. You can bind checked, as above, or leave it to the browser and read it from the form when it's sent. A checked box sends its name with its value, or with on when it has no value, and an unchecked box sends nothing at all, so check whether its name is there, rather than for a value of false. That also means the form works before your JavaScript loads.

import type { FormEvent } from "react";

export function Preferences() {
  function save(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    // An unchecked box sends nothing, so it's checked when its name is there.
    api.savePreferences({ updates: data.has("updates") });
  }

  return (
    <form onSubmit={save}>
      <Checkbox name="updates" value="yes" defaultChecked>Email me product updates</Checkbox>
      <Button type="submit" color="primary">Save preferences</Button>
    </form>
  );
}

Form libraries work the same way. They register the <input> itself, through its ref or its attributes and listeners, which is why the Checkbox puts them all on the <input>. Keep in mind that a server reading the form sees no field at all for an unchecked box, so treat a missing name as "no".

With other components

  • The Checkbox Group puts Checkboxes in a <fieldset> with a <legend>, gives them one name, binds each one's checked to a list of values, and works out its select-all box's indeterminate for you. A <fieldset disabled> already disables every Checkbox inside it.
  • The Field, on our Roadmap, will render a checkbox's hint and error message, and set its aria-describedby and aria-invalid, as it will for the Input.
  • A Button that submits the form goes after the checkboxes, and takes the same size, so their text lines up.

Controllable

The Checkbox doesn't keep any state you can't reach. Its checked state is yours to bind, or to leave to the browser, and every other state is a prop or an attribute that you control:

You'll get our updates

const [updates, setUpdates] = useState(true);

<Checkbox checked={updates} onChange={(event) => setUpdates(event.target.checked)}>Email me updates</Checkbox>
<p>You'll {updates ? "get" : "miss"} our updates</p>

<Checkbox name="remember">Remember me</Checkbox>
StateHow you control it
Checkedchecked, bound to your state, or left to the browser
Indeterminateindeterminate, worked out from the options it stands for
Disableddisabled, or a <fieldset disabled> around it
Read-onlyreadonly, which sets aria-readonly on the <input> and cancels its clicks
Requiredrequired, which lands on the <input>
Invalidaria-invalid, set while its error message shows (see Required and invalid)
FocusA ref to its <input>, and focus()

When you bind checked, the box always shows the state you hold. If a change doesn't make it into your state, like an option people aren't allowed to uncheck, the box goes back to that state as soon as it renders, rather than showing a tick that your state doesn't have. When you don't bind it, the box keeps its own state, and starts out checked if you say so.

DX Checklist

RuleLevelScopeRequirementBasisCheck
checkbox/shared-vocabularyMustComponentIt uses the shared names: size, disabled, readonly, checked, indeterminate, the default slot for its label and changeOpen ComponentsReview
checkbox/typedMustComponentProps, events and slots are typed, with unions rather than stringOpen ComponentsType check
checkbox/control-attributesMustComponentAttributes, listeners and refs land on the <input>, while classes and styles land on the <label>Open ComponentsUnit test
checkbox/form-controlMustComponentIts form sends its name with its value while it's checked, and nothing while it's unchecked, whether checked is bound or notHTMLUnit test
checkbox/controllableShouldComponentchecked can be bound or left to the browser, a bound box always shows the state you hold, and every other state is a prop or an attributeOpen ComponentsUnit test

FAQ

Copyright © 2026