Indeterminate State
A parent shows the native indeterminate checkbox state when only some of its descendants are checked.
1 node checked
Show code
<TreeKit data={data} checkedIds={checkedIds} onCheckedChange={setCheckedIds} />Check just "React" below Frontend: Frontend goes indeterminate, and so does Engineering. Check "Vue" too and Frontend becomes fully checked — Engineering stays indeterminate until Backend is fully checked as well.
It's a real DOM property, not a CSS trick
indeterminate has no HTML attribute and no React prop — it's a property that must be set imperatively on the underlying <input> element. TreeKit's Checkbox component does this in a useEffect keyed on the computed state, so assistive technology and browser rendering both see the correct native tri-state checkbox, not a div styled to look like one.
Derived, not stored
TreeKit's only source of truth is the set of ids passed as checkedIds/defaultCheckedIds. Indeterminate is computed fresh from that set plus the tree structure on every render — there's no separate "indeterminate ids" state to keep in sync, which is exactly the kind of state duplication that causes subtle bugs when nodes are added or removed.
// Simplified — see the actual implementation for the full algorithm
function computeState(node): CheckedState {
if (!hasChildren(node)) {
return checkedIds.has(node.id) ? "checked" : "unchecked";
}
const childStates = node.children.map(computeState);
if (childStates.some(s => s === "indeterminate")) return "indeterminate";
if (childStates.every(s => s === "checked")) return "checked";
if (childStates.every(s => s === "unchecked")) return "unchecked";
return "indeterminate"; // mixed checked/unchecked children
}O(n), not O(n²)
computeCheckedStates, exported from the package) does this for every node in a tree in a single linear pass — not one recursive call per node — by processing nodes in reverse pre-order, which guarantees every child is visited before its parent. See Utilities.This behavior is specific to selectionPropagation.toParents being enabled (the default). Turn it off and each node's checked state is tracked independently instead — see Selection for the full cascading model.