OperantAI Workflow Design System
ComfortableCompact

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.

PropTypeDefaultDescription
checkedboolean | "indeterminate"—Controlled checked state. Omit for uncontrolled with defaultChecked.
defaultCheckedboolean—Initial checked state for uncontrolled usage.
onCheckedChange(checked: boolean | "indeterminate") => void—Fired on toggle.
disabledbooleanfalseDisables the control (opacity: 0.5, pointer-events: none).
classNamestring—Additional classes merged onto the root.

<CheckboxField>

Extends CheckboxProps with one additional required prop:

PropTypeDefaultDescription
labelReactNode—Required. Rendered next to the control inside .ods-checkbox-label.
idstringauto-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"> with aria-checked (including "mixed" for indeterminate) kept in sync automatically — screen readers announce the correct state without any manual ARIA wiring.
  • :focus-visible applies --shadow-focus, so keyboard focus is always visible.
  • Space toggles the checkbox; this is native Radix behavior, not something this wrapper adds.
  • Checkbox alone renders no visible text — always pair it with a label, either via CheckboxField or your own <label htmlFor>/aria-label/aria-labelledby. An unlabeled checkbox is a dead end for screen reader users.