Checkbox Tree
The CheckboxTree component renders a hierarchical tree where every node has a checkbox. Checking a group cascades to all of its descendants, and parent groups automatically show an indeterminate (mixed) state when only some of their descendants are selected. It supports an optional Select All row and expand/collapse of groups.
It is a compound component composed of CheckboxTree.Root, CheckboxTree.Group, and CheckboxTree.Item. Selection is controlled via selectedIds + onSelectionChange (leaf ids only); parent/child relationships are derived from JSX nesting. Enable showCount on the Root to render each group’s leaf-descendant count (e.g. “Resources (16)”) — it is computed automatically, with no count prop to maintain.
Variations
Deeply nested groups
Groups can be nested to any depth. Each level indents its content, and every group reflects the aggregate state of all leaves beneath it.
Custom group icons
Each group renders a folder icon before its label by default. Pass the icon prop to override it with any IconV2 icon name.
Disabled nodes
Set disabled on the Root to disable the whole tree, or on an individual Group/Item.
Installation
import { CheckboxTree } from '@harnessio/ui/components'
const [selectedIds, setSelectedIds] = useState<string[]>([])
return ( <CheckboxTree.Root className="w-full rounded-cn-2 border border-cn-3" selectedIds={selectedIds} onSelectionChange={setSelectedIds} defaultExpandedIds={['resources']} showSelectAll > <CheckboxTree.Group id="resources" label="Resources"> <CheckboxTree.Item id="audit-logs" label="Audit logs" /> <CheckboxTree.Item id="roles" label="Roles" /> </CheckboxTree.Group> </CheckboxTree.Root>)Anatomy
CheckboxTree is a compound component. You nest Group and Item parts inside Root, and the hierarchy is derived from that JSX nesting:
<CheckboxTree.Root selectedIds={selectedIds} onSelectionChange={setSelectedIds} showSelectAll> <CheckboxTree.Group id="resources" label="Resources"> <CheckboxTree.Group id="pipelines" label="Pipelines"> <CheckboxTree.Item id="audit-logs" label="Audit logs" /> </CheckboxTree.Group> </CheckboxTree.Group></CheckboxTree.Root>- Root — the container. It renders a scrollable
role="tree"region, owns the selection and expand/collapse state, and shares it with every part through context. WhenshowSelectAllis set, it renders a Select All row above the tree; it draws no border of its own, so pass aclassName(e.g.rounded-cn-2 border) to frame it. - Group — a collapsible branch. Each group renders a row with its own checkbox plus an expand/collapse trigger (an arrow, a
foldericon by default, and the label). Checking it cascades to every descendant; it shows a checked, unchecked, orindeterminatestate derived from its leaves. WithshowCount, the automatically-computed leaf count appears next to the label. - Item — a leaf. It renders a checkbox and a label, and its
idis what appears inselectedIds. Only leaf ids are tracked — group state is always derived.
Groups can nest to any depth, and each level indents its contents. Collapsed groups keep their descendants mounted so parent and Select All states stay accurate.
Best practices
Do
- Use a checkbox tree when items form a genuine hierarchy and people need to select across levels — not for a short, flat list of options.
- Turn on
showSelectAllwhen selecting everything at once is a common action. - Turn on
showCountwhen the size of each group helps people decide what to select. - Give each group and item a clear, scannable
label, and start the most relevant groups expanded withdefaultExpandedIds.
Don’t
- Don’t nest so deeply that the indentation becomes hard to follow — flatten the hierarchy where you can.
- Don’t rely on the tree to communicate a partial selection by the mixed checkbox alone; pair it with a count or summary when the exact selection matters.
- Don’t disable a group or item without making it clear elsewhere why it can’t be selected.
Content
- Write group and item labels as short noun phrases in sentence case.
- Keep sibling labels parallel in structure so the tree scans cleanly.
- Use the
selectAllLabelto say what “all” means in context when the default “Select All” is ambiguous.
Accessibility
CheckboxTree renders tree semantics (role="tree" with treeitem rows and group branches) and each node’s checkbox carries an accessible name.
Keyboard
- Each checkbox is in the tab order and toggles with Space; toggling a group cascades to its descendants.
- Each group’s expand/collapse trigger is a separate tab stop and activates with Enter/Space.
- Disabled groups and items are skipped by keyboard navigation.
Labeling
Rootrendersrole="tree", each row is arole="treeitem", and each group’s descendants are wrapped in arole="group".- Every checkbox gets an
aria-labelfrom itslabel(falling back to itsidwhen the label isn’t a plain string). - Groups expose
aria-expanded, and each node reflects its selection througharia-selected/aria-checked.
Focus & states
- The checkboxes and triggers use the standard keyboard focus ring, and disabled nodes render with distinct styling.
- A group whose descendants are only partially selected shows the mixed (
indeterminate) checkbox state.
Known limitations
- The tree has no accessible name by default.
role="tree"isn’t labeled out of the box.Rootforwards extra props onto the tree element, so pass anaria-label(oraria-labelledby) describing what the tree contains. - No arrow-key tree navigation. The full WAI-ARIA tree pattern moves between nodes with the arrow keys and expands/collapses with ←/→. Here you Tab between the individual checkboxes and triggers instead; there’s no single roving focus that walks the tree.
aria-selectedonly reflects the fully-checked state. A group with a mixed selection reportsaria-selected="false"even though its checkbox showsindeterminate, so the partial state is conveyed visually more clearly than it is to assistive tech.- The Select All row isn’t a tree node. It renders as a labeled checkbox above the tree rather than a
treeitem, so it sits outside the tree’s node structure.
Properties
Root
The container that owns selection and expand state and renders the tree.
Prop | Required | Default | Type |
|---|---|---|---|
| selectedIds | true | string[] | |
| onSelectionChange | true | (ids: string[]) => void | |
| defaultExpandedIds | false | string[] | |
| showSelectAll | false | false | boolean |
| selectAllLabel | false | 'Select All' | string |
| showCount | false | false | boolean |
| disabled | false | boolean | |
| className | false | string |
CheckboxTree.Root also forwards any other HTMLAttributes<HTMLDivElement> (e.g. aria-label) onto the tree element.
Group
A collapsible branch. Its state is derived from the leaves beneath it.
Prop | Required | Default | Type |
|---|---|---|---|
| id | true | string | |
| label | true | React.ReactNode | |
| disabled | false | boolean | |
| icon | false | 'folder' | IconV2NamesType |
Item
A leaf node. Its id is what appears in selectedIds.
Prop | Required | Default | Type |
|---|---|---|---|
| id | true | string | |
| label | true | React.ReactNode | |
| disabled | false | boolean |