Using CSS :indeterminate for Tri-State Checkboxes
The CSS :indeterminate pseudo-class allows developers to
style form elements that exist in an undefined or mixed state, serving
as the visual backbone for tri-state checkboxes in nested UI trees. In
hierarchical interfaces—such as file directories, category filters, and
permission managers—a parent checkbox often controls multiple child
items. When some, but not all, children are selected, the parent cannot
accurately be marked as either fully checked or completely unchecked.
This article explains how the :indeterminate pseudo-class
visually represents this third state, how it integrates with the DOM,
and how to style it effectively within nested component structures.
The Tri-State Logic in Hierarchical Trees
A standard checkbox is binary, toggling between checked
and unchecked. A tri-state checkbox introduces a third
condition—the indeterminate state—to reflect partial completion within a
hierarchy:
- Unchecked: Zero child nodes are selected.
- Checked: All child nodes are selected.
- Indeterminate: At least one child node is selected, but not all.
In UI trees, selection state propagates in two directions. When a user clicks a parent node, the action cascades downward, setting all descendants to either checked or unchecked. Conversely, when a user toggles an individual child node, the state bubbles upward: the parent inspects its direct descendants to calculate whether it should display as checked, unchecked, or indeterminate.
Setting the Indeterminate State via JavaScript
The indeterminate state cannot be set using HTML attributes alone;
there is no native indeterminate HTML attribute like
checked or disabled. Instead, it must be set
programmatically using the JavaScript DOM property:
const parentCheckbox = document.querySelector('#parent-checkbox');
parentCheckbox.indeterminate = true;When indeterminate is set to true, modern
browsers alter the native rendering of the checkbox, typically replacing
the standard checkmark with a horizontal dash or minus symbol.
Setting indeterminate = true does not change the
element's checked property. The checkbox retains its
underlying boolean checked value behind the scenes, which
dictates the form submission value. The indeterminate state is strictly
visual and behavioral.
Styling with the
:indeterminate Pseudo-Class
The CSS :indeterminate pseudo-class targets checkboxes
currently in this mixed state. This enables consistent branding and
custom component styling without requiring extra utility classes.
Native Customization
With native inputs, :indeterminate can be combined with
standard properties like accent-color:
input[type="checkbox"]:indeterminate {
accent-color: #2563eb;
}Custom Checkbox Design
For custom UI design systems that hide the native input to render
custom icons, :indeterminate pairs with the adjacent
sibling combinator (+) or the :has()
selector:
/* Hide default appearance */
.tree-checkbox input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid #6b7280;
border-radius: 4px;
display: inline-grid;
place-content: center;
}
/* Checked State: standard checkmark */
.tree-checkbox input[type="checkbox"]:checked::before {
content: "";
width: 10px;
height: 5px;
border-left: 2px solid #ffffff;
border-bottom: 2px solid #ffffff;
transform: rotate(-45deg) translate(1px, -1px);
}
.tree-checkbox input[type="checkbox"]:checked {
background-color: #2563eb;
border-color: #2563eb;
}
/* Indeterminate State: horizontal minus bar */
.tree-checkbox input[type="checkbox"]:indeterminate::before {
content: "";
width: 10px;
height: 2px;
background-color: #ffffff;
}
.tree-checkbox input[type="checkbox"]:indeterminate {
background-color: #2563eb;
border-color: #2563eb;
}Accessibility and Screen Readers
While the :indeterminate pseudo-class handles the visual
rendering, it is critical to ensure assistive technologies correctly
interpret the state. Modern screen readers generally map the JavaScript
property element.indeterminate = true to a mixed state
natively.
However, when building fully custom tree view components (such as
those using role="tree" and role="treeitem"),
you must explicitly set the ARIA attribute on the element:
<input
type="checkbox"
id="parent"
aria-checked="mixed"
/>When updating the DOM via JavaScript, ensure both the visual property and the ARIA state stay synchronized:
function updateParentState(parentInput, checkedCount, totalCount) {
if (checkedCount === 0) {
parentInput.checked = false;
parentInput.indeterminate = false;
parentInput.setAttribute('aria-checked', 'false');
} else if (checkedCount === totalCount) {
parentInput.checked = true;
parentInput.indeterminate = false;
parentInput.setAttribute('aria-checked', 'true');
} else {
parentInput.checked = false;
parentInput.indeterminate = true;
parentInput.setAttribute('aria-checked', 'mixed');
}
}This synchronization guarantees that the UI tree communicates the
mixed status accurately to visual users via the
:indeterminate CSS selector and to assistive technologies
via the accessibility tree.