import { Tree } from "@elabs-ai/components-ui";npm install @elabs-ai/components-tokens @elabs-ai/components-uiPaste 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
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
| Prop | Type | Description |
|---|---|---|
nodesrequired | TreeNode<T>[] | |
expandedIds | string[] | Controlled expanded ids |
defaultExpandedIds | string[] | Uncontrolled initial expanded ids |
onExpandedChange | (ids: string[]) => void | |
selectionMode | "single" | "multiple" | "none" | "single" | "multiple" | "none" — default "single" |
selectedIds | string[] | Controlled selected ids |
defaultSelectedIds | string[] | Uncontrolled initial selected ids |
onSelectionChange | (ids: string[]) => void | |
checkboxes | boolean | Show 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. |
virtualize | boolean | Enable 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). |
estimateRowHeight | number | Estimated row height in px used by the virtualizer. Default 32. |
overscan | number | Number of extra rows to render beyond the visible window. Default 8. |
maxHeight | number | string | CSS 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"`). |
scrollToId | string | Scroll 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). |
scrollSelectionIntoView | boolean | When `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. |
className | string |