Checkbox
An independent binary choice with a persistent visible label and mixed-state support.
- Family
- Choice
- Available import
@/components/ui/checkbox- Status
- Thorbis wrapper and live specimen ship here.
checkbox- Use
- Use when several options may be selected or a single explicit agreement is required.
- Avoid
- Do not use for immediate on/off settings or make the label non-clickable.
Use this component
GitHub source links require access to the private Thorbis design repository.
Wrapper sourceRepository and dependencies
- Value exports
Checkbox
- Type exports
CheckboxProps
Setup, styles, and dependencies
These are React wrappers in the Thorbis design repository, not an installable npm UI package. In this repository, @/ resolves to src/ through tsconfig.json.
Use the repository's package.json and package-lock.json for dependencies. Load src/styles.css once at the application root for global tokens, typography, and themes. The wrappers compose colocated StyleX owners; include the generated token CSS and extracted StyleX output.
For another project, bring the wrapper and its transitive local imports, configure the alias, install the dependencies those files use, and provide the same token and style setup. Review the source before copying; the direct imports below are not a complete dependency closure.
Interactive examples require client-side React. This TanStack Start site server-renders and hydrates React; another host must hydrate or render the component on the client. Example code is the same TypeScript source used by its live preview.
Direct imports in this wrapper:
./choice.stylex@phosphor-icons/react@radix-ui/react-checkbox@radix-ui/react-slot@stylexjs/stylexreact
Evidence choices, mixed state and form reset
Choose completion evidence, confirm that you reviewed it, and preview or reset the local result. Nothing is saved or sent. Checkbox supplies its own checked/mixed indicator and keeps a native button with a 40px desktop or 44px small-screen/coarse-pointer target. Its visible label belongs outside the compact face; an associated wrapping Label makes the whole choice row clickable. Use checked and onCheckedChange for controlled values, or defaultChecked for an uncontrolled choice. The checked state can be true, false or the string indeterminate; the callback receives that state, not a native change event. Named checked choices submit their value (on by default); unchecked, mixed and disabled choices are omitted. Set form to a form element’s ID when the button sits outside it; named values and native reset follow that form. The form owner resets controlled choices, while native form reset restores an uncontrolled default. asChild accepts one button or a component that renders a button and passes through every prop, children and its React 19 ref; it preserves the supplied child and adds the indicator. Keep checkbox state/form props on Checkbox. Connect persistent labels, help and errors explicitly with IDs; the ref points to the button for focus. This noValidate example owns visible required-confirmation feedback. Interactive controls stay disabled with guidance until hydration.
Read the complete example
import { exampleLayout } from "@/components/components/examples/example-layout.stylex";
import { useEffect, useId, useRef, useState, type ComponentProps, type FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { FormActions, FormError, FormField, FormHelp } from "@/components/ui/form";
import { Label } from "@/components/ui/label";
// A composed child keeps the native button and passes props, children and the React 19 ref through.
function EvidenceCheckboxButton(props: ComponentProps<"button">) {
return <button {...props} />;
}
export function JobCheckboxExample() {
const id = useId();
const confirmation = useRef<HTMLButtonElement>(null);
const [ready, setReady] = useState(false);
const [photos, setPhotos] = useState(true);
const [signature, setSignature] = useState(false);
const [error, setError] = useState(false);
const [preview, setPreview] = useState("");
useEffect(() => setReady(true), []);
const all = photos && signature ? true : !photos && !signature ? false : "indeterminate";
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
if (!data.has("reviewed")) {
setError(true);
setPreview("");
confirmation.current?.focus();
return;
}
setError(false);
const selected = data
.getAll("evidence")
.map((value) => (value === "photos" ? "completion photos" : "customer signature"));
setPreview(
`Preview evidence: ${selected.join(" and ") || "no optional evidence"}. Nothing was saved or sent.`,
);
}
return (
<form
aria-label="Job evidence example"
aria-busy={!ready}
{...exampleLayout.stack}
noValidate
onSubmit={submit}
onReset={() => {
// The state owner restores controlled choices; the confirmation uses native form reset.
setPhotos(true);
setSignature(false);
setError(false);
setPreview("");
}}
>
<fieldset
{...exampleLayout.choices}
disabled={!ready}
aria-describedby={`${id}-evidence-help`}
>
<legend {...exampleLayout.legend}>Completion evidence</legend>
<Label htmlFor={`${id}-all`} {...exampleLayout.row}>
<Checkbox
id={`${id}-all`}
asChild
checked={all}
disabled={!ready}
onCheckedChange={(checked) => {
// Reset can restore the mixed state; the form owner restores its individual choices.
if (checked === "indeterminate") return;
setPhotos(checked);
setSignature(checked);
setPreview("");
}}
>
<EvidenceCheckboxButton />
</Checkbox>
<span {...exampleLayout.shrink}>Select all completion evidence</span>
</Label>
<Label htmlFor={`${id}-photos`} {...exampleLayout.row}>
<Checkbox
id={`${id}-photos`}
name="evidence"
value="photos"
checked={photos}
disabled={!ready}
onCheckedChange={(checked) => {
setPhotos(checked === true);
setPreview("");
}}
/>
<span {...exampleLayout.shrink}>Require completion photos</span>
</Label>
<Label htmlFor={`${id}-signature`} {...exampleLayout.row}>
<Checkbox
id={`${id}-signature`}
name="evidence"
value="signature"
checked={signature}
disabled={!ready}
onCheckedChange={(checked) => {
setSignature(checked === true);
setPreview("");
}}
/>
<span {...exampleLayout.shrink}>
Require the customer’s signature before the technician leaves the property
</span>
</Label>
<FormHelp id={`${id}-evidence-help`}>
{ready
? "Choose the evidence to collect. A mixed mark means only some choices are selected."
: "The interactive example is loading. If it remains unavailable, reload the page with JavaScript enabled."}
</FormHelp>
</fieldset>
<FormField>
<Label htmlFor={`${id}-permit`} {...exampleLayout.row} data-disabled="true">
<Checkbox
id={`${id}-permit`}
name="permit"
defaultChecked
disabled
aria-describedby={`${id}-permit-help`}
/>
<span {...exampleLayout.shrink}>Require permit attachment</span>
</Label>
<FormHelp id={`${id}-permit-help`}>
Permit attachment is unavailable in this local example.
</FormHelp>
</FormField>
<FormField>
<Label htmlFor={`${id}-reviewed`} {...exampleLayout.row}>
<Checkbox
ref={confirmation}
id={`${id}-reviewed`}
name="reviewed"
defaultChecked={false}
required
disabled={!ready}
aria-invalid={error || undefined}
aria-describedby={`${id}-reviewed-help`}
onCheckedChange={() => {
setError(false);
setPreview("");
}}
/>
<span {...exampleLayout.shrink}>I reviewed these evidence requirements</span>
</Label>
{error ? (
<FormError id={`${id}-reviewed-help`}>
Confirm that you reviewed the requirements.
</FormError>
) : (
<FormHelp id={`${id}-reviewed-help`}>
Review the choices before previewing. Confirmation is required.
</FormHelp>
)}
</FormField>
<FormActions>
<Button type="submit" disabled={!ready}>
Preview evidence
</Button>
<Button type="reset" variant="ghost" disabled={!ready}>
Reset example
</Button>
</FormActions>
<p role="status" aria-live="polite">
{preview || "This example changes only its local preview."}
</p>
</form>
);
}
Adoption contract
- Start from the ShadCN accessible source primitive; keep upstream keyboard and focus behavior.
- Use Thorbis color, type, space, radius, shadow, and motion tokens only.
- Design hover, focus, selected, disabled, loading, empty, error, offline, and reduced-motion states where they apply.
- Verify desktop and 390px mobile, keyboard order, accessible names, and stable async geometry.