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.