Checkbox wraps @radix-ui/react-checkbox's Root + Indicator. Radix drives data-state="checked" | "unchecked" | "indeterminate" on the root element, and css/components.css keys its visuals directly off that attribute — no parallel class system, per AGENTS.md #2.
Installation
HTML — already included if you loaded the 3 stylesheets. See the Installation guide. Note: the checkable, tri-state behavior below (data-state, indeterminate) is Radix-driven in the React layer — a hand-written <input type="checkbox"> only gets you two states and needs your own script to add a third.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
A plain native checkbox styled with .ods-checkbox covers the two-state case with zero JS:
<input type="checkbox" class="ods-checkbox" aria-label="Accept terms" />
React
import { Checkbox } from "@operant/ui";
export default function Example() {
return <Checkbox aria-label="Accept terms" />;
}
Examples
Default / checked
<input type="checkbox" class="ods-checkbox" aria-label="Accept terms" />
<input type="checkbox" class="ods-checkbox" aria-label="Accept terms" checked />
<Checkbox aria-label="Accept terms" />
<Checkbox aria-label="Accept terms" defaultChecked />;
Indeterminate
Radix's Checkbox.Root accepts checked="indeterminate", which sets data-state="indeterminate" — styled identically to checked (.ods-checkbox[data-state="checked"], .ods-checkbox[data-state="indeterminate"] share the filled background). Native HTML has no indeterminate value for checkboxes — only the DOM .indeterminate boolean property, which must be set imperatively via JS and is not reflected in CSS by an attribute selector, so this state is React/Radix-only here.
<Checkbox aria-label="Select all" checked="indeterminate" />
Disabled
<input type="checkbox" class="ods-checkbox" aria-label="Accept terms" disabled />
<Checkbox aria-label="Accept terms" disabled />
With a label — CheckboxField
CheckboxField pairs Checkbox with an .ods-checkbox-label, auto-generating and wiring an id/htmlFor pair via useId() when you don't pass your own id.
<label class="ods-checkbox-label" for="updates">
<input type="checkbox" class="ods-checkbox" id="updates" />
Send me product updates
</label>
import { CheckboxField } from "@operant/ui";
<CheckboxField label="Send me product updates" />;
API Reference
<Checkbox>
Forwards every prop from Radix's CheckboxPrimitive.Root (checked, defaultChecked, onCheckedChange, disabled, required, name, value, etc.) — no props of its own beyond className.
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | "indeterminate" | — | Controlled checked state. Omit for uncontrolled with defaultChecked. |
defaultChecked | boolean | — | Initial checked state for uncontrolled usage. |
onCheckedChange | (checked: boolean | "indeterminate") => void | — | Fired on toggle. |
disabled | boolean | false | Disables the control (opacity: 0.5, pointer-events: none). |
className | string | — | Additional classes merged onto the root. |
<CheckboxField>
Extends CheckboxProps with one additional required prop:
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Required. Rendered next to the control inside .ods-checkbox-label. |
id | string | auto-generated via useId() | Used to wire the <label htmlFor> to the checkbox. |
HTML-side modifiers: the visual states (unchecked/checked/indeterminate) are selected via data-state in the React/Radix version. A plain <input type="checkbox"> styled with .ods-checkbox only expresses checked/unchecked through the native :checked pseudo-class path that Radix's CSS also happens to key off visually for the base states; there's no -danger/-sm style modifier for this component.
Accessibility
- The React component renders Radix's checkbox as a real
<button role="checkbox">witharia-checked(including"mixed"for indeterminate) kept in sync automatically — screen readers announce the correct state without any manual ARIA wiring. :focus-visibleapplies--shadow-focus, so keyboard focus is always visible.- Space toggles the checkbox; this is native Radix behavior, not something this wrapper adds.
Checkboxalone renders no visible text — always pair it with a label, either viaCheckboxFieldor your own<label htmlFor>/aria-label/aria-labelledby. An unlabeled checkbox is a dead end for screen reader users.