Skip to content

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.

Data Display22 propsAccessibility notes

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

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

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

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

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.