Skip to content

Selection

Checkbox

Lets people select one or more items from a list, or turn an option on or off, with indeterminate and error states.

Examples

States

Unchecked, checked, indeterminate, invalid and disabled. Indeterminate is a property, not an attribute, which is why the element exists.

The live demo runs with JavaScript. Its code is below.

React HTML
import { Checkbox } from '@kyralabs/m3exp-web/react';

export default function States() {
  return (
    <div style={{ display: 'grid' }}>
      <Checkbox>Unchecked</Checkbox>
      <Checkbox defaultSelected>Checked</Checkbox>
      <Checkbox isIndeterminate>Indeterminate</Checkbox>
      <Checkbox isInvalid>Invalid</Checkbox>
      <Checkbox isDisabled defaultSelected>
        Disabled
      </Checkbox>
    </div>
  );
}
<div style="display: grid">
  <m3e-checkbox>Unchecked</m3e-checkbox>
  <m3e-checkbox checked>Checked</m3e-checkbox>
  <m3e-checkbox indeterminate>Indeterminate</m3e-checkbox>
  <m3e-checkbox invalid>Invalid</m3e-checkbox>
  <m3e-checkbox checked disabled>Disabled</m3e-checkbox>
</div>

Run the HTML version (custom elements, no framework).

API

React

<Checkbox>

Checkbox (COMPONENT_SPEC §22): React Aria useCheckbox / react-stately useToggleState on the m3e-css .m3e-checkbox contract, the same DOM as <m3e-checkbox>. The ref is the native input. The check draws and morphs in CSS (checkbox.css); this writes data-from (the previous state, so indeterminate from unchecked draws the dash) and the ripple.

Prop Type Default Description
children ReactNode The label (Compose's CheckboxWithTextSample row: the whole label toggles).
isSelected boolean Controlled checked state.
isIndeterminate boolean Shows the dash (Compose TriStateCheckbox ToggleableState.Indeterminate).
isInvalid boolean Error colors (aria-invalid).
className string
defaultSelected boolean
isDisabled boolean
isReadOnly boolean
isRequired boolean
name string
value string
id string
autoFocus boolean
aria-label string
aria-labelledby string
aria-describedby string
aria-controls string
onChange (isSelected: boolean) => void Called with the new checked state.
onPressStart (e: PressEvent) => void
onPressEnd (e: PressEvent) => void
onFocus (e: React.FocusEvent) => void
onBlur (e: React.FocusEvent) => void

Custom elements

<m3e-checkbox>

import '@kyralabs/m3e-elements/checkbox.js';

<m3e-checkbox checked indeterminate disabled invalid required name value label>Label</m3e-checkbox> (COMPONENT_SPEC §22): renders the m3e-css checkbox inside itself (the host is display: contents) around a native <input type="checkbox">, which takes part in forms and fires the native input / change events. It exists for what HTML cannot declare:

  • indeterminate is only an input property; the attribute sets it.
  • data-from on the .m3e-checkbox root keeps the previous state, so a checkbox that turns indeterminate from unchecked draws the dash instead of morphing the check (Compose snaps the gravitation there).
  • The ripple (the 40dp circle, pressed from the label or the input; Space only).

The text content is the label (else name it with label, the input's aria-label). invalid sets aria-invalid (error colors), required aria-required. A user toggle updates checked and clears indeterminate, as the native input does.

Attribute Property Type Notes
checked checked boolean
indeterminate indeterminate boolean
disabled disabled boolean
invalid invalid boolean
required required boolean
name name string
value value string
label label string

CSS

checkbox.css

@import '@kyralabs/m3e-css/checkbox.css';

Checkbox (COMPONENT_SPEC §22): a native checkbox, checked / unchecked / indeterminate, error, disabled. No JavaScript needed (the native indeterminate property is the one thing HTML cannot declare: set it from script, or use <m3e-checkbox indeterminate> / m3e-react).

<label class="m3e-checkbox">
  <input class="m3e-checkbox__input" type="checkbox" />
  <span class="m3e-checkbox__box" aria-hidden="true">
    <svg class="m3e-checkbox__mark" focusable="false">
      <path class="m3e-checkbox__stroke" d="M0 0H48" /><path class="m3e-checkbox__stroke" d="M0 0H48" />
    </svg>
  </span>
  <span class="m3e-checkbox__state" aria-hidden="true"></span>
  <span class="m3e-checkbox__label">Option selection</span>
</label>

The label text is optional (then name the input with aria-label). Error: aria-invalid="true" on the input. The JS layers also write data-from="<previous state>" on the root, so a checkbox that becomes indeterminate from unchecked draws the dash instead of morphing the check (below).

54 custom properties it reads
Custom property Default
--m3e-comp-checkbox-border-color-checked var(--m3e-sys-color-primary)
--m3e-comp-checkbox-border-color-unchecked var(--m3e-sys-color-on-surface-variant)
--m3e-comp-checkbox-border-color-unchecked-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-checkbox-border-opacity-unchecked-disabled 0.38
--m3e-comp-checkbox-box-color-checked var(--m3e-sys-color-primary)
--m3e-comp-checkbox-box-color-checked-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-checkbox-box-corner-radius 2px
--m3e-comp-checkbox-box-opacity-checked-disabled 0.38
--m3e-comp-checkbox-box-size 20px
--m3e-comp-checkbox-box-size-styling-fix 18px
--m3e-comp-checkbox-check-cross-x 0.4
--m3e-comp-checkbox-check-cross-y 0.7
--m3e-comp-checkbox-check-cross-y-styling-fix 0.65
--m3e-comp-checkbox-check-duration var(--m3e-sys-motion-spring-default-spatial-duration)
--m3e-comp-checkbox-check-easing var(--m3e-sys-motion-spring-default-spatial-easing)
--m3e-comp-checkbox-check-gravitation-center 0.5
--m3e-comp-checkbox-check-left-x 0.2
--m3e-comp-checkbox-check-left-x-styling-fix 0.25
--m3e-comp-checkbox-check-left-y 0.5
--m3e-comp-checkbox-check-right-x 0.8
--m3e-comp-checkbox-check-right-x-styling-fix 0.75
--m3e-comp-checkbox-check-right-y 0.3
--m3e-comp-checkbox-checkmark-color var(--m3e-sys-color-on-primary)
--m3e-comp-checkbox-checkmark-color-disabled var(--m3e-sys-color-on-primary)
--m3e-comp-checkbox-checkmark-color-disabled-styling-fix var(--m3e-sys-color-surface)
--m3e-comp-checkbox-color-in-duration var(--m3e-sys-motion-spring-default-effects-duration)
--m3e-comp-checkbox-color-in-easing var(--m3e-sys-motion-spring-default-effects-easing)
--m3e-comp-checkbox-color-out-duration var(--m3e-sys-motion-spring-fast-effects-duration)
--m3e-comp-checkbox-color-out-easing var(--m3e-sys-motion-spring-fast-effects-easing)
--m3e-comp-checkbox-container-color-selected-error var(--m3e-sys-color-error)
--m3e-comp-checkbox-follow-duration 1ms
--m3e-comp-checkbox-icon-color-selected-error var(--m3e-sys-color-on-error)
--m3e-comp-checkbox-label-space 16px
--m3e-comp-checkbox-label-text-font var(--m3e-sys-typescale-body-large-font)
--m3e-comp-checkbox-label-text-line-height var(--m3e-sys-typescale-body-large-line-height)
--m3e-comp-checkbox-label-text-size var(--m3e-sys-typescale-body-large-size)
--m3e-comp-checkbox-label-text-tracking var(--m3e-sys-typescale-body-large-tracking)
--m3e-comp-checkbox-label-text-weight var(--m3e-sys-typescale-body-large-weight)
--m3e-comp-checkbox-outline-color-unselected-error var(--m3e-sys-color-error)
--m3e-comp-checkbox-snap-delay 100ms
--m3e-comp-checkbox-snap-easing steps(1, jump-end)
--m3e-comp-checkbox-state-layer-color currentcolor
--m3e-comp-checkbox-state-layer-color-checked-styling-fix var(--m3e-sys-color-primary)
--m3e-comp-checkbox-state-layer-color-error-focus var(--m3e-sys-color-error)
--m3e-comp-checkbox-state-layer-color-error-hover var(--m3e-sys-color-error)
--m3e-comp-checkbox-state-layer-color-error-pressed var(--m3e-sys-color-error)
--m3e-comp-checkbox-state-layer-color-unselected-focus var(--m3e-sys-color-on-surface)
--m3e-comp-checkbox-state-layer-color-unselected-hover var(--m3e-sys-color-on-surface)
--m3e-comp-checkbox-state-layer-color-unselected-pressed var(--m3e-sys-color-primary)
--m3e-comp-checkbox-state-layer-shape var(--m3e-sys-shape-corner-full)
--m3e-comp-checkbox-state-layer-size 40px
--m3e-comp-checkbox-stroke-width 2px
--m3e-comp-checkbox-styling-fix 1
--m3e-comp-minimum-interactive-component-size 48px

Accessibility

native <input type="checkbox"> (works without JS; label association; Space toggles); indeterminate is the input property; error = aria-invalid, required = aria-required (React Aria's default validation behavior).

Every interactive part has a target of at least 48 × 48 px, a visible focus ring for keyboard focus only, and works under forced-colors and prefers-reduced-motion.

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §22 and the m3e-css sources.

CheckboxTokens.kt@19991f2c plus the Checkbox.kt / CheckboxDefaults values Compose renders (components/checkbox-defaults.css; SOURCES register #39). ComposeMaterial3Flags.isCheckboxStylingFixEnabled (false at the pin) picks between two stylings; both are generated and --m3e-comp-checkbox-styling-fix selects one. Owner decision (M8): flag on (1), which matches the tokens and m3.material.io: 18dp box (CheckboxTokens.ContainerSize; flag off: 20dp), 2dp corners, 2dp stroke (CheckboxDefaults.StrokeWidth) with square caps, check path left (0.25, 0.5) → cross (0.4, 0.65) → right (0.75, 0.3) of the box (flag off: 0.2 / 0.7 / 0.8), centred in the 48dp minimum interactive size; 40dp state layer circle (StateLayerSize, unbounded ripple of radius 20dp from the centre). Colors (defaultCheckboxColors): checked / indeterminate box and border primary, checkmark on-primary; unchecked border on-surface-variant, box transparent; disabled checked / indeterminate on-surface at 0.38 with a surface checkmark (flag off: on-primary), disabled unchecked border on-surface at 0.38. Ripple color = the box color when checked / indeterminate (indicatorColor: primary, error with an error; flag off: the content color). While unchecked Compose's flag-on ripple is transparent (no state layer); owner decision (2026-10-17, amending M8 #4 for unchecked only): the m3.material.io token DB colors instead (TOKEN_TABLE.31f16b44fe03b0c0: md.comp.checkbox.unselected.{hover,focus}.state-layer.color on-surface, .unselected.pressed primary; with an error md.comp.checkbox.error.{hover,focus,pressed}.state-layer.color error), at the shared state layer opacities. The DB's selected pressed color (on-surface) differs from Compose's box color and is not applied. Error (not a Compose parameter; owner decision M8): CheckboxTokens Selected/UnselectedError* (error box / outline, on-error checkmark). Hover, focus and press change no box or outline color: the tokens' Unselected{Hover,Focus,Pressed}OutlineColor (on-surface) are never rendered by Compose, so only the state layer shows (owner decision M8). Label: Compose's CheckboxWithTextSample, 16dp after the 48dp checkbox, body large, content color; the whole label toggles and shows the checkbox's ripple.

checkbox.css sources

(compose/material3 @ 19991f2c, Checkbox.kt; values in checkbox.css / checkbox-defaults.css, lines in PARITY.md):

  • ComposeMaterial3Flags.isCheckboxStylingFixEnabled (L41-49 there) picks between two stylings. --m3e-comp-checkbox-styling-fix mixes them (0 = Compose's default, flag off: 20dp box, check points 0.2 / 0.7 / 0.8, disabled checkmark on-primary, ripple in the content color; 1 = flag on: CheckboxTokens.ContainerSize 18dp, 0.25 / 0.65 / 0.75, disabled checkmark surface, ripple in the box color, transparent while unchecked). Owner decision (M8): 1, which matches the token file and m3.material.io. One declaration (e.g. on :root) switches it back.
  • State layer while unchecked (owner decision 2026-10-17, amending M8 #4; flag on only): not Compose's transparent ripple but the m3.material.io token DB's per-state colors (md.comp.checkbox.unselected.{hover,focus}.state-layer.color on-surface, .pressed primary; with aria-invalid md.comp.checkbox.error.* error). Checked / indeterminate keep the box color.
  • Layout (L554-593): clickable → minimumInteractiveComponentSize (48dp, the input here), triStateToggleable (Role.Checkbox), the box centred in it. Unbounded ripple of radius StateLayerSize / 2 (L554-560): the 40dp state layer circle, the press grows from its centre.
  • drawBox (L675-701): one fill when box == border color, else the fill inside the outline and the outline: a border-box with a padding-box background here (outer radius 2dp, inner 0).
  • drawCheck (L704-737): the path left → cross → right, its points lerped towards the centre by the gravitation fraction (indeterminate: a straight dash), drawn up to length × the draw fraction (PathMeasure.getSegment) with square caps. Here an inline SVG with one path per segment, placed and turned by CSS (atan2() of the gravitated points) and drawn by stroke-dashoffset; at the check's right angle the two square-capped segments cover exactly the miter join. The mark is an animated indicator, not a system glyph icon (owner decision M8), so CLAUDE.md's Icon rule does not apply. It is not mirrored in RTL (Compose draws it in canvas coordinates).
  • Motion (L595-631): draw fraction and gravitation on DefaultSpatial (registered properties on each stroke, transitioned with the spring's linear() easing); towards unchecked both snap after SnapAnimationDelay (a one-step easing over the delay); leaving unchecked the gravitation snaps (data-from='unchecked' here). The strokes' 1ms "follow" transitions only make WebKit repaint them while the fractions animate (checkbox-defaults.css follow-duration). Colors (L633-650, L1040-1049): DefaultEffects in, FastEffects out; box and border snap while disabled. The checkmark color goes from Color.Transparent to its color and back, so it fades by opacity here (one layer: the two segments overlap at the joint).
  • Colors: defaultCheckboxColors (L446-476); indeterminate = checked. Error is not a Compose parameter: the CheckboxTokens Selected/UnselectedError* colors (owner decision M8). Hover, focus and press change no box or outline color (the tokens' Unselected{Hover,Focus,Pressed} OutlineColor are never rendered by Compose): state layer only.
  • Label: CheckboxWithTextSample (CheckboxSamples.kt L62-89): 16dp after the 48dp checkbox, bodyLarge, in the content color; the whole row toggles and shows the checkbox's ripple.