Skip to main content

Tree

Navigationnavigation

A hierarchical list with expand and collapse, single or multiple selection and full keyboard navigation.

import { Tree } from "@elabs-ai/components-ui";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui Tree component (from @elabs-ai/components-ui) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look Tree up before writing code: `npx -y @elabs-ai/components-cli docs Tree` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-ui is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Examples

14 live examples

Multiple

Multiple selection with checkboxes.

Expand On Chevron

`expandOn="chevron"`: a click on a branch row only SELECTS it — expanding is the chevron's (and ArrowRight's) job. The shape a filter tree wants, where a parent value is selectable in its own right.

Expanded

Fully expanded at mount via defaultExpandedIds.

Collapsed

Collapsed — no nodes expanded at start.

Disabled Nodes

Nodes with disabled items.

Deep Nesting

Deep nesting — 4 levels.

Controlled

Controlled expand + select with external state display.

No Selection

No selection mode — purely navigational.

Virtualized

5 000 flat nodes rendered with `virtualize`. Only the visible window (~15–25 rows) is mounted in the DOM at any time; keyboard navigation and End/Home work across all 5 000 items.

Reveal Selected Node

Sidebar Surface

`surface="sidebar"` renders the selected row with the sidebar's own active-item treatment (`bg-sidebar-accent` + `text-sidebar-accent-foreground` + `font-medium`), matching `SidebarMenuButton`'s active state — because the default `bg-selection` fill is near-invisible against `--sidebar`. No fill is ≥3:1 on a sidebar, so (like the Sidebar itself) perceivability comes from the foreground/weight shift plus `aria-selected`, not fill contrast alone. NOTE: cross-theme visual correctness (every shipped theme — light, dark) requires a real render and is owed to a human reviewer; the tokens are semantic so they adapt, but pixel-level confirmation cannot be automated in this environment.

Lazy Loading

Nodes marked `hasChildren: true` with no loaded `children` show the expand chevron. On first expand, `loadChildren` is called; a skeleton loading row appears under `aria-busy` while the request is in flight. Resolved children are cached — re-expanding does not re-fetch. "Category C" simulates a network error and shows the inline retry affordance.

Trailing Accessory

Per-node trailing content — e.g. a file-size badge — via `TreeNode.accessory` (#369). It renders as a SIBLING of the label, outside the row's interactive label region: it never joins the row's accessible name and clicking it never selects or expands the row.

API

Tree

PropTypeDescription
nodesrequiredTreeNode<T>[]
expandedIdsstring[]Controlled expanded ids
defaultExpandedIdsstring[]Uncontrolled initial expanded ids
onExpandedChange(ids: string[]) => void
selectionMode"single" | "multiple" | "none""single" | "multiple" | "none" — default "single"
selectedIdsstring[]Controlled selected ids
defaultSelectedIdsstring[]Uncontrolled initial selected ids
onSelectionChange(ids: string[]) => void
checkboxesbooleanShow checkboxes for multi-selection affordance — default false
surface"default" | "sidebar"The surface on which the tree is rendered. - `"default"` (default) — renders selected rows with `bg-selection` (for use on background/card surfaces). - `"sidebar"` — renders selected rows with the sidebar's own active-item treatment (`bg-sidebar-accent` + `text-sidebar-accent-foreground` + `font-medium`), so selection reads consistently with `SidebarMenuButton`'s active state on the `--sidebar` surface. (No fill is ≥3:1 on a sidebar; perceivability comes from the foreground/weight shift + `aria-selected`, the same multi-cue approach the Sidebar uses.)
expandOn"row" | "chevron"What a click on a row does to an expandable node. `"row"` (default): the click both selects the node and toggles its children. `"chevron"`: the click only selects; only the chevron (and ArrowLeft/ArrowRight) expands or collapses — the filter-pane convention, where clicking a parent value must select it without reflowing the list.
virtualizebooleanEnable windowed rendering for large trees (>50 visible rows). When `true`, only the visible slice of the flattened tree is mounted in the DOM. Requires a bounded scroll container — either via `maxHeight` or by sizing the root element externally with `overflow-auto`. Default: `false` (backward-compatible nested-group DOM).
estimateRowHeightnumberEstimated row height in px used by the virtualizer. Default 32.
overscannumberNumber of extra rows to render beyond the visible window. Default 8.
maxHeightnumber | stringCSS max-height for the scroll viewport when `virtualize` is true. E.g. `"400px"` or `"60vh"`. When omitted the root div must be sized by the caller (e.g. via `className="h-96 overflow-auto"`).
scrollToIdstringScroll the node with this id into view when the value CHANGES. Reveals a node selected programmatically (via `selectedIds`) that is windowed out of the DOM in `virtualize` mode (or below the fold), so the tree shows "where I am", not just structure. Expands the node's (loaded) ancestors and kicks their lazy `loadChildren` as needed, then centers the node. Independent of keyboard focus (keyboard nav's own scroll is unchanged).
scrollSelectionIntoViewbooleanWhen `true`, reveal the newly-selected controlled node (the id added to `selectedIds`) into view — the selection-driven equivalent of `scrollToId`. `scrollToId` wins when both are set.
loadChildren(node: TreeNode<T>) => Promise<TreeNode<T>[]>Async function called when a node with `hasChildren: true` (and no loaded `children`) is expanded for the first time. The returned nodes are merged into the internal tree state and cached so re-expanding the node does not re-fetch.
classNamestring

Also accepts: Omit<HTMLAttributes<HTMLDivElement>, "onSelect">