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 Display25 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())}
/>

Moving rows

Pass onMove and rows can be dragged before, after or into another row, or moved from the keyboard with Alt and the arrow keys (up and down among siblings, right into the row above, left out of the parent). The callback gets { key, parentKey, index }; apply it to your nodes with moveTreeNode, which returns a new tree. canMove limits which rows can be picked up and canDrop which moves are allowed.

import { TreeView, moveTreeNode } from "oks-ui/tree-view";

const [pages, setPages] = useState(nodes);

<TreeView
  aria-label="Pages"
  nodes={pages}
  onMove={(move) => setPages((current) => moveTreeNode(current, move))}
  canDrop={(move) => move.parentKey !== "readme"}
/>

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.

onMove

(move: TreeMove) => void

default: —

Lets rows be dragged before, after or into another row, or moved with Alt and the arrow keys. move is { key, parentKey, index }; apply it with moveTreeNode(nodes, move).

canMove

(node: TreeNode) => boolean

default: every enabled row

Which rows can be picked up (with onMove).

canDrop

(move: TreeMove) => boolean

default: every move

Whether a move is allowed (with onMove). A row never moves into itself or its own children.

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

Partial<TreeViewLabels>

default: English

loading, loadFailed(name), and with onMove: moveHint, moved, cannotMove. Also set app-wide with LocaleProvider.

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.
  • With onMove, Alt and the arrow keys move the focused row and every move is announced; a hidden hint linked to the tree explains the keys. Escape cancels a pointer drag.
  • 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.
  • Moving rows is opt-in: pass onMove. Give people a way to undo, and use canDrop to keep rows out of places they do not belong.