TreeView component
TreeView shows a hierarchy you can open, close, select and, if you want, check: a file browser, a category picker, a permissions tree, an outline. It follows the WAI-ARIA tree pattern, supports single or multiple selection, check marks that roll up to parents, and children that load the first time a branch opens. It is for data; for site or app navigation use Nav.
Playground
Change a prop on the right and the preview updates. Switch to Code to copy what you see.
Selecting rows
Give it a nodes array of { key, label, children? }. Keys must be unique across the whole tree. selectionMode is single by default; use multiple for several rows or none for a tree that only opens and closes. Control the selection with selectedKeys and onSelectionChange, and the open branches with expandedKeys and onExpandedChange (or their default versions).
const nodes: TreeNode[] = [
{ key: "docs", label: "Documents", children: [
{ key: "cv", label: "CV.pdf" },
{ key: "work", label: "Work", children: [{ key: "plan", label: "Plan.md" }] },
] },
{ key: "readme", label: "README" },
];
<TreeView aria-label="Files" nodes={nodes} defaultExpandedKeys={["docs"]} onSelectionChange={(keys) => open(keys[0])} />Check marks that roll up
isCheckable adds a check box to every row. Checking a parent checks everything under it, and a parent shows a dash when only some children are checked. onCheckedChange reports every fully checked node, parents included. A disabled node is shown but does not decide its parent's mark.
<TreeView
aria-label="Permissions"
nodes={nodes}
isCheckable
selectionMode="none"
onCheckedChange={(keys) => save(keys)}
/>Children that load on demand
Mark a node hasChildren and give loadChildren: it is called the first time the node opens, a spinner shows on the row, and the result is cached. If it fails, onLoadError is called, the branch closes and opening it again retries. Both outcomes are announced to screen readers.
<TreeView
aria-label="Remote"
nodes={[{ key: "root", label: "Server", hasChildren: true }]}
loadChildren={(node) => fetch(`/api/children/${node.key}`).then((r) => r.json())}
/>Props
The full public API for TreeView. Every prop is stable and intentionally typed.
Data
| Prop | Type | Default | Description |
|---|---|---|---|
| nodes | TreeNode[] | — | The tree: { key, label, children?, hasChildren?, icon?, isDisabled?, textValue?, data? }. Keys are unique across the whole tree. |
| selectedKeys / defaultSelectedKeys | Iterable<string> | — | Selection, controlled or not. |
| onSelectionChange | (keys: string[]) => void | — | Called when the selection changes. |
| expandedKeys / defaultExpandedKeys | Iterable<string> | — | Which branches are open. |
| onExpandedChange | (keys: string[]) => void | — | Called when a branch opens or closes. |
| checkedKeys / defaultCheckedKeys | Iterable<string> | — | Checked rows. A branch in the list counts as everything under it. |
| onCheckedChange | (keys: string[]) => void | — | Reports every fully checked node, parents included. |
nodes
TreeNode[]
default: —
The tree: { key, label, children?, hasChildren?, icon?, isDisabled?, textValue?, data? }. Keys are unique across the whole tree.
selectedKeys / defaultSelectedKeys
Iterable<string>
default: —
Selection, controlled or not.
onSelectionChange
(keys: string[]) => void
default: —
Called when the selection changes.
expandedKeys / defaultExpandedKeys
Iterable<string>
default: —
Which branches are open.
onExpandedChange
(keys: string[]) => void
default: —
Called when a branch opens or closes.
checkedKeys / defaultCheckedKeys
Iterable<string>
default: —
Checked rows. A branch in the list counts as everything under it.
onCheckedChange
(keys: string[]) => void
default: —
Reports every fully checked node, parents included.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| selectionMode | "none" | "single" | "multiple" | single | What a click selects. |
| isCheckable | boolean | false | Adds a check mark to every row. A parent shows a dash when only some children are checked. |
| expandOnClick | boolean | false | Clicking a branch row opens it as well as selecting it. |
| onAction | (node: TreeNode) => void | — | Enter or a double click. |
| loadChildren | (node) => Promise<TreeNode[]> | — | Called the first time a node with hasChildren and no children opens. |
| onLoadError | (node, error) => void | — | Loading failed: the branch closes again, opening it retries. |
| isDisabled | boolean | false | The whole tree. A single node can have isDisabled. |
selectionMode
"none" | "single" | "multiple"
default: single
What a click selects.
isCheckable
boolean
default: false
Adds a check mark to every row. A parent shows a dash when only some children are checked.
expandOnClick
boolean
default: false
Clicking a branch row opens it as well as selecting it.
onAction
(node: TreeNode) => void
default: —
Enter or a double click.
loadChildren
(node) => Promise<TreeNode[]>
default: —
Called the first time a node with hasChildren and no children opens.
onLoadError
(node, error) => void
default: —
Loading failed: the branch closes again, opening it retries.
isDisabled
boolean
default: false
The whole tree. A single node can have isDisabled.
Content
| Prop | Type | Default | Description |
|---|---|---|---|
| renderLabel | (node, state) => ReactNode | — | Draw a row's label yourself. state has level, isExpanded, isSelected, isFocused, isLoading, isBranch and checkState. |
| emptyContent | ReactNode | — | Shown when nodes is empty. |
| labels | { loading, loadFailed(name) } | English | The spoken loading messages. |
| aria-label / aria-labelledby | string | — | One is needed: it names the tree. |
renderLabel
(node, state) => ReactNode
default: —
Draw a row's label yourself. state has level, isExpanded, isSelected, isFocused, isLoading, isBranch and checkState.
emptyContent
ReactNode
default: —
Shown when nodes is empty.
labels
{ loading, loadFailed(name) }
default: English
The spoken loading messages.
aria-label / aria-labelledby
string
default: —
One is needed: it names the tree.
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | md | Row height and text size. |
| color | "default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger" | primary | Selected row, focus ring and check marks. |
| indent | number | 20 | Pixels each level moves in. |
| classNames | Partial<Record<slot, string>> | — | Class per part: base, item, toggle, checkbox, icon, label, empty. |
size
"sm" | "md" | "lg"
default: md
Row height and text size.
color
"default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger"
default: primary
Selected row, focus ring and check marks.
indent
number
default: 20
Pixels each level moves in.
classNames
Partial<Record<slot, string>>
default: —
Class per part: base, item, toggle, checkbox, icon, label, empty.
Accessibility
- role="tree" holds role="treeitem" rows with aria-level, aria-setsize, aria-posinset, aria-expanded on branches, aria-selected, aria-checked (true, false or mixed) when checkable, and aria-busy while loading.
- One tab stop. Arrow Up and Down move, Arrow Right opens a branch and then goes into it, Arrow Left closes it and then goes to the parent (swapped in right-to-left pages), Home and End jump, Enter selects and acts, Space selects or checks, * opens every sibling branch, and typing letters jumps to the next matching row.
- Give a node a textValue when its label is not plain text, so type-ahead and screen readers have a name for it.
- Reduced motion removes the chevron turn, and forced-colors mode outlines the selected row.
Best practices
- Give the tree a name with aria-label or aria-labelledby; it is required.
- Use TreeView for data (files, categories, permissions) and Nav for navigation.
- There is no virtual scrolling, so a tree with several thousand open rows renders them all; load deep levels with loadChildren instead of opening everything.
- There is no drag and drop; if you need reordering, offer move actions in a menu.