Checkbox
A control that toggles between checked, unchecked, and indeterminate.
import { Div, Label } from "@implementjs/core";
import { Checkbox } from "@/lib/components/ui/checkbox";
export default function CheckboxDemo() {
return Div(
{ class: "flex flex-col gap-4" },
Div(
{ class: "flex items-center gap-2" },
Checkbox({ id: "terms" }),
Label(
{ for: "terms", class: "text-sm leading-none font-medium" },
"Accept terms and conditions",
),
),
Div(
{ class: "flex items-center gap-2" },
Checkbox({ id: "emails", checked: true }),
Label(
{ for: "emails", class: "text-sm leading-none font-medium" },
"Send me product updates",
),
),
Div(
{ class: "flex items-center gap-2" },
Checkbox({ id: "partial", indeterminate: true }),
Label(
{ for: "partial", class: "text-sm leading-none font-medium" },
"Select all notifications",
),
),
Div(
{ class: "flex items-center gap-2" },
Checkbox({ id: "disabled", checked: true, disabled: true }),
Label(
{
for: "disabled",
class:
"text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
},
"Disabled",
),
),
);
}A checkbox is a button that turns a value on and off. Checkbox renders a Button with role="checkbox" and keeps aria-checked in sync — you give it a size, a border, and the indicator that shows inside it.
import { Checkbox } from "@implementjs/primitives";
Checkbox();
It accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent. Extra props are forwarded onto the underlying Button.
Checked state
checked defaults to false. Pass a boolean to seed it, or a signal to control it from outside (signal() returns a writable unchanged, so the same prop accepts both):
const accepted = signal(false);
Checkbox({ checked: accepted });
Button({ onClick: () => accepted.set(true) }, "Accept");
Clicking the checkbox toggles checked. Space and Enter do the same, because it is a real Button.
onCheckedChange runs on every change to checked, so you can react without holding a signal of your own:
Checkbox({ onCheckedChange: (checked) => console.log(checked) });
Indeterminate
indeterminate is for a parent that represents a mixed set — some children on, some off. Pass a boolean or a signal. While it is true, data-state is "indeterminate" and aria-checked is "mixed".
Clicking an indeterminate checkbox clears that flag and checks it:
const all = signal(false);
const mixed = signal(true);
Checkbox({ checked: all, indeterminate: mixed });
Labels
Pair it with a Label whose for matches the checkbox id. Clicking the text then toggles the control, and the accessible name comes from the label instead of the indicator:
import { Label } from "@implementjs/core";
Div(
{ class: "flex items-center gap-2" },
Checkbox({ id: "terms" }),
Label({ for: "terms" }, "Accept terms and conditions"),
);
Forms
Pass name and the checkbox renders a visually hidden native input that submits with the form. The value is "on" while checked; pass value to change it. required and disabled apply to that input too:
Form(
{ method: "post" },
Checkbox({ name: "terms", value: "accepted" }),
Button({ type: "submit" }, "Continue"),
);
Without name, no hidden input is rendered. An indeterminate checkbox submits as unchecked.
Decoration
Inside something that is already the control — a menuitemcheckbox row, a selectable card — a second checkbox would put two checked states on one thing. decorative draws the box without the semantics: a Span rather than a Button, no role, aria-checked, or aria-required, and aria-hidden over the lot, so nothing tabs to it and assistive tech reads the control around it instead.
Checkbox({ decorative: true, checked });
data-state and the click toggle stay, so it looks and behaves like every other checkbox — the only difference is that it is not one. Submitting belongs to the real control: name renders no hidden input here, and disabled is not forwarded.
What it shows has to come from the state that already exists, and a two-way bind is usually the shortest way there — a Signal<boolean> view of a value's place in a list, with no second copy to keep in sync:
const selected = signal(["ui-fix"]);
Checkbox({
decorative: true,
checked: selected.bind(
(labels) => labels.includes("bug"),
(labels, checked) => (checked ? [...labels, "bug"] : labels.filter((l) => l !== "bug")),
),
});
The dropdown menu draws its label rows exactly this way.
Styling
The primitive is unstyled until you give it a look. It sets data-checkbox-root and data-state as "checked", "unchecked", or "indeterminate", so one class list can cover every state. Children are the indicator — typically a check icon that you show while checked, and a minus while mixed:
import { CheckIcon, MinusIcon } from "@implementjs/lucide";
Checkbox(
{
class:
"size-4 rounded-[4px] border border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
},
CheckIcon({ class: "size-3.5 hidden [[data-state=checked]_&]:block" }),
MinusIcon({ class: "size-3.5 hidden [[data-state=indeterminate]_&]:block" }),
);
data-state is there for the fill, the icon, and anything else that should react to checked versus mixed without you threading a signal through.
API Reference
Checkbox
A toggle that is checked, unchecked, or indeterminate. Sets role="checkbox" and aria-checked. Give it a look and an indicator; it handles the state. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
checked | Signal<boolean> | boolean | false | The checked state. Pass a signal to control it from outside; a boolean seeds uncontrolled state. |
onCheckedChange | (checked: boolean) => void | — | Runs whenever the checked state changes. |
indeterminate | Signal<boolean> | boolean | false | Partial selection. While true, data-state is "indeterminate" and aria-checked is "mixed". A click clears it and checks the box. |
onIndeterminateChange | (indeterminate: boolean) => void | — | Runs whenever the indeterminate state changes. |
name | string | — | If set, a hidden checkbox is rendered so the value submits with a form. |
value | string | "on" | The value submitted while checked. Only used when name is set. |
required | boolean | false | Marks the hidden input as required. Sets aria-required on the button. |
decorative | boolean | false | Draws the box inside something that is already the control. Renders a Span instead of a Button, drops role, aria-checked, and aria-required, and sets aria-hidden. data-state and the click toggle stay; name renders no hidden input and disabled is not forwarded. |
| Data attribute | Value |
|---|---|
[data-checkbox-root] | Present |
[data-state] | "checked" | "unchecked" | "indeterminate" |