Checkbox Tree — Tri-State Parent/Child Checkboxes

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than assuming checkbox.indeterminate works like a normal attribute, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why indeterminate can only be set via JavaScript and never via HTML markup, and why refreshParent() has to run after every single child change rather than just once. The same assistant can help optimize it — ask whether the expand/collapse height animation's scrollHeight-then-auto-on-transitionend pattern could glitch if a group's content changes size while it's mid-animation, and how you'd guard against that. It's also useful for extending the tree: ask it to support a third level of nesting (grandchildren under children), add a "select all / clear all" button that operates on the whole tree at once, or persist the checked state to localStorage so a permissions selection survives a page reload. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a nested "tri-state checkbox tree" in plain HTML, CSS, and JavaScript — no framework, no tree library.

Requirements:
- A two-level tree (parent items each containing a group of child checkboxes) marked up with proper tree ARIA roles (role="tree", role="treeitem", role="group"), where every checkbox is a real input inside a label so clicking the visible text also toggles it.
- Each parent checkbox must reflect three states based on its children: fully checked when all children are checked, unchecked when none are, and indeterminate (a visually distinct dash state, not just unchecked) when only some children are checked — set via the checkbox's indeterminate DOM property, which cannot be expressed in HTML alone.
- Checking or unchecking a parent must cascade that same value down to every one of its children and clear the parent's own indeterminate flag.
- Checking or unchecking any child must recompute its parent's tri-state (checked/unchecked/indeterminate) by counting how many siblings are currently checked — this must work correctly no matter which child was toggled.
- Each parent group must expand and collapse with a smooth height animation triggered by a caret button: since CSS cannot transition to height: auto, measure the group's scrollHeight, animate to that pixel value, and only set height back to auto once the transition completes (so the group can still reflow if its content changes later) — and do the reverse when collapsing.
- Maintain a live count of selected leaf-level (child) checkboxes displayed in the UI, updating on every change.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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.
Category and tag pickers
Select nested categories alongside a faceted filter sidebar.
File and folder selection
Pick items in a file manager UI with parent folders.
Notification preferences
Group channels under topics in a notification preferences panel.
Column and field toggles
Show/hide grouped fields, like a column toggle.
Learning tri-state checkboxes
A reference for indeterminate state and two-way propagation.

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.