webdevpuneet.com

Checkbox Tree — Tri-State Parent/Child Checkboxes

Checkbox Tree · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="ck-card">
  <h3>Select permissions</h3>
  <ul class="ck-tree" id="ckTree" role="tree">
    <li role="treeitem" data-id="content">
      <div class="ck-row"><button class="ck-tog" type="button" aria-label="Toggle"></button><label class="ck-lbl"><input type="checkbox"><span>Content</span></label></div>
      <ul class="ck-children" role="group">
        <li role="treeitem" data-id="content-read"><div class="ck-row"><span class="ck-spacer"></span><label class="ck-lbl"><input type="checkbox"><span>Read articles</span></label></div></li>
        <li role="treeitem" data-id="content-write"><div class="ck-row"><span class="ck-spacer"></span><label class="ck-lbl"><input type="checkbox"><span>Write &amp; edit</span></label></div></li>
        <li role="treeitem" data-id="content-publish"><div class="ck-row"><span class="ck-spacer"></span><label class="ck-lbl"><input type="checkbox"><span>Publish</span></label></div></li>
      </ul>
    </li>
    <li role="treeitem" data-id="billing">
      <div class="ck-row"><button class="ck-tog" type="button" aria-label="Toggle"></button><label class="ck-lbl"><input type="checkbox"><span>Billing</span></label></div>
      <ul class="ck-children" role="group">
        <li role="treeitem" data-id="billing-view"><div class="ck-row"><span class="ck-spacer"></span><label class="ck-lbl"><input type="checkbox"><span>View invoices</span></label></div></li>
        <li role="treeitem" data-id="billing-manage"><div class="ck-row"><span class="ck-spacer"></span><label class="ck-lbl"><input type="checkbox"><span>Manage payment methods</span></label></div></li>
      </ul>
    </li>
  </ul>
  <p class="ck-out" id="ckOut">0 selected</p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Tri-state parents
Parents show checked, unchecked, or indeterminate from their children.
Cascade down
Ticking a parent selects or clears every child.
Propagate up
Child changes recompute the parent's mixed state.
Real indeterminate
Uses the DOM indeterminate property, shown as a dash.
Animated groups
Height-animated expand/collapse with a rotating caret.
Tree ARIA roles
role=tree/treeitem/group with native label checkboxes.
Live selection count
A summary reflects how many leaves are selected.
No library
Pure HTML/CSS/JS — no tree or form component.

About this UI Snippet

Checkbox Tree — Indeterminate Tri-State Parent and Child Checkboxes

Screenshot of the Checkbox Tree snippet rendered live

A checkbox tree is a hierarchy of checkboxes where ticking a parent selects all its children, and a parent shows an indeterminate state when only some children are ticked. It's the control for permission grids, category pickers, and file selectors. This snippet builds an accessible, animated one with correct tri-state logic and collapsible groups, in plain HTML, CSS, and vanilla JavaScript.

The tri-state that everyone gets wrong

A parent checkbox has three visual states, not two: checked (all children on), unchecked (none on), and indeterminate (some on). The indeterminate state is a real DOM property (checkbox.indeterminate = true) — it can't be set in HTML, only JavaScript — and it shows as a dash rather than a tick. After any child change, refreshParent counts the ticked children and sets the parent to checked, unchecked, or indeterminate accordingly, so the parent always summarises its group at a glance.

Two-way propagation

Selection flows both directions. Ticking a parent cascades down: every child is set to match and the parent's indeterminate flag clears. Ticking a child propagates up: the parent recomputes its tri-state. Keeping both directions in sync is what makes the control feel coherent — you can select a whole group with one click, then untick one item and watch the parent flip to the dash automatically.

Animated expand/collapse

Each group expands and collapses with a height animation. Because you can't transition to height: auto, the script measures scrollHeight, animates to that pixel value, then sets height: auto on transitionend so the group can still reflow if content changes — and reverses the trick to collapse. A rotating caret indicates the open/closed state. Groups start expanded so the full hierarchy is visible.

Accessible structure

The tree uses role="tree", role="treeitem", and role="group", and every checkbox is a real <input> inside a <label>, so clicking the text toggles it and screen readers announce the checked/mixed state natively. accent-color themes the checkboxes without replacing them, keeping native keyboard behaviour (Space to toggle, Tab to move) for free.

Reporting the selection

A live summary counts the selected leaf items, demonstrating how to read the tree's value — in real use you'd collect the checked leaves' ids to submit. The whole component is compact and dependency-free, a clean reference for the tri-state checkbox-tree pattern that's surprisingly easy to get subtly wrong.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA permissions tree renders with expandable parent groups.
  2. 2
    Tick a parentChecking a parent selects all of its children at once.
  3. 3
    Tick some childrenThe parent shows an indeterminate dash when only some are on.
  4. 4
    Expand or collapseClick the caret to animate a group open or closed.
  5. 5
    Read the selectionThe summary counts selected leaves; collect their ids to submit.
  6. 6
    Extend the treeAdd more treeitem groups — the logic works per parent.

Real-world uses

Common Use Cases

Permission grids
Assign grouped capabilities in a settings panel, with parents ticking all children and showing a dash when only some are selected.
Category and tag filters
Select nested categories alongside a faceted filter sidebar, using the real DOM indeterminate property for mixed parents.
File and folder selection
Pick items inside a file manager UI, with child changes propagating upward to recompute each parent.
Notification preferences
Group channels under topics in notification preferences, with expand and collapse keeping a long tree manageable.
Column visibility toggles
Show or hide grouped fields like a data table column toggle, cascading down and recalculating up in one pass.
Related: CSS color-mix() Playground
See the CSS color-mix() Playground for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It is a third visual state — shown as a dash — that means "some but not all children are selected." It is not available in HTML; you set it in JavaScript with checkbox.indeterminate = true. Here a parent becomes indeterminate whenever the number of ticked children is greater than zero but less than the total, so the parent always summarises its group accurately.

Both ways. Ticking a parent cascades down — every child is set to match and the parent's indeterminate flag clears. Ticking a child propagates up — the parent recounts its children and updates to checked, unchecked, or indeterminate. Keeping both directions synchronised is what makes the tree behave predictably.

You cannot CSS-transition to height: auto, so to animate a group open the script measures its scrollHeight, transitions to that pixel value, then sets height: auto on transitionend so the group can reflow later. Collapsing reverses it. This gives a smooth expand/collapse while still letting the content resize naturally when expanded.

Collect the checked leaf inputs — the ones inside .ck-children — and read their data-id (or value). The parent checkboxes are summaries, so you usually submit only the leaves. The demo counts them for the summary line; in a form you would gather the ids into an array or a hidden field.

Model the tree as data (nodes with children and a checked flag) and derive each parent's checked/indeterminate from its children in render. Set the indeterminate DOM property via a ref/directive since it is not a normal attribute — useEffect in React, a ref in Vue, or [indeterminate] binding in Angular. Update children on parent change and recompute parents on child change. Tailwind users swap the classes for utilities.