Checkbox
stimeo--checkbox
Adds parent and child linkage and the partly-selected state to ordinary checkboxes.
Fills in what a browser checkbox cannot express on its own. From the state of the children, the parent becomes all, some or none, and the partly-selected state is announced properly to screen readers. Pressing the parent sets every child at once. A parent is optional: a group of children alone still publishes which of the three states it is in, for CSS to use. When checkboxes come and go, when Turbo swaps the markup, or when the form is reset, the state is worked out again from the children. The tick itself and the look of partly-selected are drawn by this demo's CSS.
1 selected
Keyboard
| Key | Action |
|---|---|
| Space | Toggle the focused checkbox (native). |
<%# Markup for the checkbox (APG Checkbox / 3-state) demo.
The parent ("select all") derives checked / indeterminate from the children's
state, and a parent toggle sets all children at once. The aggregate state is
reflected on the root's data-state (all/partial/none); the look is the consumer's CSS. %>
<div class="checkbox-demo" role="group" aria-labelledby="cb-group-label"
data-controller="stimeo--checkbox">
<span class="checkbox-demo__legend" id="cb-group-label">
<%= t("components.checkbox.demo.group") %>
</span>
<label class="checkbox-demo__row checkbox-demo__row--parent">
<input type="checkbox" data-stimeo--checkbox-target="parent"
data-action="change->stimeo--checkbox#onParentChange" />
<span><%= t("components.checkbox.demo.all") %></span>
</label>
<div class="checkbox-demo__children">
<label class="checkbox-demo__row">
<input type="checkbox" checked data-stimeo--checkbox-target="child"
data-action="change->stimeo--checkbox#onChildChange" />
<span><%= t("components.checkbox.demo.item_a") %></span>
</label>
<label class="checkbox-demo__row">
<input type="checkbox" data-stimeo--checkbox-target="child"
data-action="change->stimeo--checkbox#onChildChange" />
<span><%= t("components.checkbox.demo.item_b") %></span>
</label>
<label class="checkbox-demo__row">
<input type="checkbox" data-stimeo--checkbox-target="child"
data-action="change->stimeo--checkbox#onChildChange" />
<span><%= t("components.checkbox.demo.item_c") %></span>
</label>
</div>
<%# Subscribe to the change event and show the selected count (updated by demo.js). %>
<p class="checkbox-demo__status" aria-live="polite">
<span data-checkbox-count>1</span> <%= t("components.checkbox.demo.selected") %>
</p>
</div>
/*
* Presentation-only styles for the checkbox demo.
* The aggregate state is available on the root's data-state (all/partial/none).
*/
.checkbox-demo {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0;
border: 0;
max-width: 22rem;
}
.checkbox-demo__legend {
font-weight: 600;
margin-bottom: 0.25rem;
}
.checkbox-demo__row {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-demo__row input {
width: 1.1rem;
height: 1.1rem;
accent-color: var(--accent);
}
.checkbox-demo__row--parent {
font-weight: 600;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--border);
}
.checkbox-demo__children {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding-left: 1.25rem;
}
.checkbox-demo__status {
margin: 0.5rem 0 0;
font-size: 0.85rem;
color: var(--color-text-muted);
}
// Demo script: subscribe to the checkbox change event and show how many children are checked.
document.addEventListener('stimeo--checkbox:change', function () {
const checked = document.querySelectorAll(
'.checkbox-demo [data-stimeo--checkbox-target="child"]:checked',
).length;
const out = document.querySelector('[data-checkbox-count]');
if (out) out.textContent = String(checked);
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--checkbox"
Targets
| Name | Description | Attribute |
|---|---|---|
parent
|
Optional (maximum one). The "select all" checkbox whose checked/indeterminate state is derived from the children. | data-stimeo--checkbox-target="parent" |
child
|
An individual checkbox in the group, including disabled targets, counted to compute the aggregate state. | data-stimeo--checkbox-target="child" |
Actions
| Name | Description | Action |
|---|---|---|
onChildChange
|
Recomputes the parent's checked/indeterminate from the children on a child change. | stimeo--checkbox#onChildChange |
onParentChange
|
Cascades the parent's checked state to every child and clears its indeterminate. | stimeo--checkbox#onParentChange |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires for parent/child change actions; detail { checked, indeterminate, state } where state is all/partial/none. |
stimeo--checkbox:change |
reconcile
|
Fires when child churn, a checked-attribute change, a morph, or a form reset moves the aggregate; same detail as change. | stimeo--checkbox:reconcile |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
indeterminate |
Parent | true when only some children are checked (announced as "mixed"). |
data-state |
Root | "all" / "partial" / "none" — the aggregate selection state. |