import { TreeChart } from "@elabs-ai/components-charts";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-chartsPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui TreeChart chart (from @elabs-ai/components-charts) 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 TreeChart up before writing code: `npx -y @elabs-ai/components-cli docs TreeChart` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-charts is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts`). 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.
Use it for
- How does it connect or flow? Membership in a hierarchy.
- a hierarchy read as a branching tree rather than as sized rectangles
- a metric decomposed into driver metrics — a KPI or driver tree
Avoid
- size, not structure, is the point — use a treemap
- Reaching for TreeChart when the real question is 'how big is each part' — that is TreemapChart's job (leaf AREA proportional to value). A tree answers only 'what contains what'.
- Encoding a value in node size or area — in a renderNode card too: every custom node is the same nodeWidth × nodeHeight box; put the number INSIDE the card as text and restate it through datapointLabel.
- Putting buttons or links inside renderNode — the content is aria-hidden and inert; the chart owns the open/close pill and the keyboard tree, and onDatapointClick is the drill-in.
- Expecting the layout to shrink to fit its container — it lays out at FIXED spacing and scrolls inside its box instead of compressing into illegibility.
- Reaching for collapseDepth — it is deprecated: use defaultExpandedDepth (or expandedIds + onExpandedChange); only collapsible={false} keeps the static chart and its '+k' pill.
Works with
Examples
Default
Forty nodes in a 400×320 card: the tree keeps its spacing and scrolls inside the card instead of shrinking to fit.
Loading
`status="loading"` (RM-184): the skeleton + `ChartLoadingLabel`, until the data arrives.
Tooltip Avoids Node
Regression lock (RM-184 review, F05): `ChartTooltipBox` receives the hovered node's own hit rect as `avoid`, so the tooltip box never lands back over the node it describes — the same policy every other chart's mark hover keeps. Forty nodes, not two: a tree this small always has room beside the pointer to fit inside the chart, so the box reliably takes the "inside" placement pass — the only pass `avoid` can change. A two-node tree left no room, so the box always took the "escape" pass instead and the lock passed whether or not `avoid` was wired up at all.
Top To Bottom
`orientation="tb"` — root on top, growing down; the same before/after label convention rotates onto the vertical growth axis.
Categorical
`palette="categorical"` — one hue per top-level branch; the root stays neutral since it belongs to no branch.
Collapsed
Start with the branches closed: `defaultExpandedDepth={1}` opens only the root, so each package shows how many components it holds, like “tokens (3)”, beside a ringed dot. Click a dot to open it, or Tab in and use the arrow keys: Right opens a branch, Left closes it.
Interactive
Click a node — or Tab in and press Enter — to report it below.
Orientation
Switch between left to right and top to bottom: the same tree glides into its new layout, and any branch you closed stays closed.
Custom Nodes
Draw each node as your own card with `renderNode`, sized by `nodeWidth` and `nodeHeight`. The chart adds the open/close pill on the side the tree grows towards, in either direction. Cards stay the same size, whatever they show: a tree answers what contains what, never how big each part is. Each card’s facts are restated for screen readers through `datapointLabel`.
Link Decorations
`renderLink` draws something at the midpoint of every link — here each team's share of the people in the team above it; in a finance driver tree the operator a child enters its parent with (+, −, ×). It rides along when branches open, close or the tree reorients, and it is presentational: put the same fact into `datapointLabel` so the tree items say it too.
Viewport
`zoom` + `minimap` turn the box into a canvas, the way `CanvasShell` works in the flow package: the wheel zooms around the pointer, dragging pans — from the empty canvas or from a card, even when the whole tree fits — a trackpad pinch zooms, and the corner holds zoom in / out / fit and a minimap you can click or drag to move the view. A click on a card still opens or closes it. Zoom runs 0.5–2 by default (`zoomRange`); the keyboard tree, the pills and the flights work at every zoom.
Controlled
Own the open branches yourself: pass `expandedIds` and `onExpandedChange`, and drive them from anywhere, such as these “Expand all” and “Collapse all” buttons.
Static
`collapsible={false}` draws the static chart: no open/close, no tree to Tab through. `collapseDepth={1}` then folds everything past the package level into one “+k” pill, where k counts the leaves it hides.
API
TreeChart
| Prop | Type | Description |
|---|---|---|
messages | ChartMessages | messages group (RM-187): this chart's own words, keyed by the ui catalogue's `charts.*` message keys. A key set here wins over the `LocaleProvider`; every other key reads the catalogue as before. |
datarequired | TreeNode<TData> | The hierarchy. Nodes need no `value`: membership only. |
orientation | TreeOrientation | `"lr"` (default) or `"tb"`. Both work with default and custom nodes. |
nodeSize | number | Node dot diameter, in px. Default `7`. Ignored with `renderNode`. |
palette | TreePalette | Which colour family shades a node. Default `"mono"`. |
collapsible | boolean | Branches open and close. Default `true`. `false` draws a static chart with every branch open (or cut by `collapseDepth`), no toggles and no tree keyboard model; with `onDatapointClick`, nodes are plain data-point buttons as on the other charts. The static chart always draws default dots: `renderNode` needs `collapsible`. |
expandedIds | string[] | Open branches, by node id (controlled). Pair with `onExpandedChange`. |
defaultExpandedIds | string[] | Open branches on first render (uncontrolled). Wins over `defaultExpandedDepth`. |
onExpandedChange | (ids: string[]) => void | Fires with the full list of open branch ids, in depth-first order, after every open or close. |
defaultExpandedDepth | number | Uncontrolled start: branches shallower than this depth start open (root = depth `0`, so `1` shows the root's children closed). Default: every branch open. Branches that appear later, when `data` changes, follow the same rule. |
collapseDepth | number | @deprecated Use `defaultExpandedDepth`. While the chart is collapsible it means exactly that: nodes AT this depth start closed, showing `(n)`. With `collapsible={false}` it keeps its old behaviour, replacing everything deeper with a single "+k" pill (k = hidden leaves). |
renderNode | (node: TreeChartNodeRenderProps<TData>) => ReactNode | Draws each node yourself, inside a fixed `nodeWidth` × `nodeHeight` box. The output is presentational only: it sits in an `aria-hidden` wrapper, so put nothing focusable in it, and restate its facts through `datapointLabel`. The chart adds the expand/collapse pill. Needs the collapsible chart: with `collapsible={false}` it is ignored (with a warning in development) and default dots are drawn. |
renderLink | (link: TreeChartLinkRenderProps<TData>) => ReactNode | Draws something at the midpoint of each link, centred on the line — the operator a child contributes with (`+`, `−`, `×`), a weight, a role. The output is presentational only: it sits in an `aria-hidden` wrapper, so put nothing focusable in it, and restate what it says through `datapointLabel` / `accessibleDescription`. Rides along when branches open, close or reorient. Works with default dots and with `renderNode`. |
zoom | boolean | A canvas viewport, as on `CanvasShell`: the wheel zooms around the pointer, dragging pans (from the empty canvas or from a node; a click still opens a node), the tree can be dragged around even when it fits, a trackpad pinch zooms, and zoom in / out / fit controls sit in the corner. Default `false`: the chart scrolls inside its box at its natural size. |
zoomable | boolean | A canvas viewport (wheel zoom, drag pan, pinch, corner controls). @deprecated Since 6.0.0, use `zoom`. Removed in 7.0.0. |
zoomRange | [min: number, max: number] | The zoom range with `zoom`. Default `[0.5, 2]` (React Flow's). |
defaultZoom | number | The zoom on first render with `zoom`. Default `1`. |
onZoomChange | (zoom: number) => void | Fires after every zoom change (buttons, wheel, pinch, fit). |
minimap | boolean | A minimap in the corner: every node as a box, the part of the tree in view as a window; click or drag on it to move the view. Default `false`. |
nodeWidth | number | Custom node box width, px. Default `160`. Only used with `renderNode`. |
nodeHeight | number | Custom node box height, px. Default `72`. Only used with `renderNode`. |
plotAlign | "start" | "center" | `"start"` (default): the tree sits at the top-left of its container. `"center"`: centred in a larger container. A tree bigger than its container scrolls (it is never clipped) and opens centred on its root; changes that no one toggled, such as "Expand all" or an orientation switch, keep it centred there. |
align | "start" | "center" | Where the tree sits when smaller than its container. @deprecated Since 6.0.0, use `plotAlign`. Removed in 7.0.0. |
className | string | |
plotHeight | Responsive<ChartPlotHeight> | A fixed plot height (or responsive per-tier value), so the chart sizes itself instead of relying on a wrapping element's CSS height. Unset: the chart takes its container's natural size and scrolls inside it — its long-standing default (see the module docblock). |
accessibleLabel | ChartA11yProps["accessibleLabel"] | Accessible name for the chart region (and its tree). |
accessibleDescription | ChartA11yProps["accessibleDescription"] | Supplemental description read by AT. |
status | ChartStatus | Show the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`. |
onDatapointClick | ChartDatapointClickHandler<Record<string, unknown>> | Fires when a datapoint is activated by pointer OR keyboard. Setting it is what enables the interaction layer — with it unset the chart renders exactly as before, with no extra DOM and no new focusables. |
copyValueOnActivate | boolean | Put the datapoint's EXACT value on the clipboard when it is activated — the recovery path for compact axis labels and tooltips, reachable by mouse AND keyboard because activation rides `ChartDatapointLayer`'s real buttons. Default `false` on a raw chart container: turning it on mounts the interaction layer, and the documented guarantee is that a chart with no interaction props renders byte-identical DOM. `AutoChart` — the spec-driven surface, where the caller never sees the axis config — turns it on by default. A consumer-supplied `onDatapointClick` always wins; the two never both run. |
datapointLabel | ChartDatapointLabel<Record<string, unknown>> | Override the accessible name of each keyboard target. |
maxInteractiveDatapoints | number | Dev-warning threshold on the number of keyboard targets. Default 500. NOT a functional cap — every plotted point stays reachable, so a keyboard user can reach exactly what a mouse user can click. |
TreemapChart
| Prop | Type | Description |
|---|---|---|
messages | ChartMessages | messages group (RM-187): this chart's own words, keyed by the ui catalogue's `charts.*` message keys. A key set here wins over the `LocaleProvider`; every other key reads the catalogue as before. |
datarequired | TreemapNode | The hierarchy. A leaf needs a `value`; a parent's explicit `value` (if any) must equal the sum of its children (dev-validated — see `validateTreemapData`). |
depth | 1 | 2 | How many levels of rectangles to render. `1` = flat (root's children only, no title bands). `2` = groups + leaves. Default `2`. |
palette | TreemapPalette | - `"mono"` (default) — one neutral shade for every leaf; groups separated only by the title band + gap. - `"sequential"` — leaf shade encodes the leaf's own value. - `"categorical"` — one hue per top-level group, ≤ 4 groups (else `"mono"`). |
gap | number | Paper seam between tiles, in px. Default `2`. |
labelMinArea | number | Hide a tile's label below this area (px²). Never shrinks type — a label either renders at `text-chart-value`/`text-chart-source` size, or not at all. |
labelOverflow | "ellipsis" | "hide" | How a name too long for its tile is handled (#280). - `"ellipsis"` (default) — clip it to the tile with `…`. - `"hide"` — draw it only when the WHOLE name fits (and, with `labels`, its value line too); otherwise draw nothing. Applies to leaf labels and group title bands alike — a truncated header is the same noise as a truncated tile name. |
hideLeafLabel | (leaf: TreemapLeafDatum) => boolean | Skip the native name/value label for every leaf this predicate matches (#280) — for a tile a caller annotates itself (a `Leader` + `HaloText` callout) so the two labels never double up. Default `undefined` — every eligible leaf keeps its built-in label. |
monoLeafColor | string | Override for `palette="mono"`'s one leaf shade (#280). The mono ramp is the same absolute lightness in every theme by design (`on-mark-ink.ts`), so it can look pale on a dark card; resolve a theme-appropriate step (`resolveThemeIsDark` + `var(--chart-mono-N)`) and pass it here when the default reads too light against your card. Default: the shared constant. |
monoBandColor | string | Override for the `depth: 2` group title band's fill — same reasoning as {@link monoLeafColor}. |
otherThreshold | number | Merge leaves under this share (0..1) of their parent's total into "Other". `0` (default) = off — the unconditional 30-leaf cap still applies. |
drilldown | boolean | When `true` (and `depth: 2`), clicking a group's title band zooms into that group (its leaves fill the canvas, animated); a "Back" control returns to the overview. Default `false` — a static chart has no click handler beyond `onDatapointClick`. |
showValues | boolean | @deprecated Use `labels` — `true`/`false` keep meaning the same thing (ADR 0042 A.3, row 15). Read until 7.0.0, with one development warning; when both are set, `labels` wins. |
labels | boolean | ChartDataLabelsConfig | Print each labelled tile's formatted value on a second line under its name (#247). A tile shows the value only when its name is drawn AND it is tall enough for two lines and wide enough for the whole number — a value is never ellipsised or shrunk, it is either legible or absent. All visible values share ONE notation (the set is formatted together). Default `false` — area stays the only printed quantity unless you opt in. |
valueFormat | ChartValueFormat | How leaf values render in the tooltip and (with `labels`) on tiles. Default `"compact"`. |
className | string | |
style | CSSProperties | |
aspectRatio | string | Aspect ratio as "width / height". Default `"16 / 9"`. |
plotHeight | Responsive<ChartPlotHeight> | The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Wins over `aspectRatio`, which stays an alias. |
accessibleLabel | ChartA11yProps["accessibleLabel"] | Accessible name for the chart region (announces to AT on focus). |
accessibleDescription | ChartA11yProps["accessibleDescription"] | Supplemental description read by AT. |
legend | ContainerLegendProp | Renders a legend (RM-118). What it shows depends on `palette`: - `"categorical"` — one row per top-level GROUP, via `useContainerLegend` (placement + hover only, R3): hovering a row dims every OTHER group's tiles, never hides one. `depth: 2`'s title bands already NAME each group on the tile itself — this adds the colour key, not a second name. `{ values: true }` prints each group's total, in `valueFormat`. - `"sequential"` — leaf shade encodes a continuous VALUE, not a discrete category, so this renders `RampLegend` (the shared ramp key every sequential/diverging consumer uses — `ramp-legend.tsx`) instead, with its marker following the hovered leaf's value. - `"mono"` (default) — every leaf is the SAME shade; there is nothing to key, so a truthy `legend` renders nothing. Unset renders nothing (R1). |
status | ChartStatus | Show the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`. |
empty | ChartEmptyState | Title and message shown when there is nothing to plot. |
selectionStates | ChartSelectionStatesResolver<Record<string, unknown>> | Maps a mark (category, optional series key, optional raw row) to its selection state. Unset → the chart renders exactly as before. |
dimExcluded | boolean | Dim `excluded` marks and add their non-hue channel (the dashed frame). Default `true`; `false` keeps the `data-selection` attribute (so a host can style it) but paints nothing. |
onDatapointClick | ChartDatapointClickHandler<Record<string, unknown>> | Fires when a datapoint is activated by pointer OR keyboard. Setting it is what enables the interaction layer — with it unset the chart renders exactly as before, with no extra DOM and no new focusables. |
copyValueOnActivate | boolean | Put the datapoint's EXACT value on the clipboard when it is activated — the recovery path for compact axis labels and tooltips, reachable by mouse AND keyboard because activation rides `ChartDatapointLayer`'s real buttons. Default `false` on a raw chart container: turning it on mounts the interaction layer, and the documented guarantee is that a chart with no interaction props renders byte-identical DOM. `AutoChart` — the spec-driven surface, where the caller never sees the axis config — turns it on by default. A consumer-supplied `onDatapointClick` always wins; the two never both run. |
datapointLabel | ChartDatapointLabel<Record<string, unknown>> | Override the accessible name of each keyboard target. |
maxInteractiveDatapoints | number | Dev-warning threshold on the number of keyboard targets. Default 500. NOT a functional cap — every plotted point stays reachable, so a keyboard user can reach exactly what a mouse user can click. |
HaloText
| Prop | Type | Description |
|---|---|---|
halo | string | The halo colour. Defaults to `var(--chart-background)` — the plot ground — which is what makes the mark THEME-SAFE: on a light card the halo is light, on a dark card it is dark, with no `dark:` override and no literal anywhere. Override it only when the text sits on something other than the plot ground (a tooltip plate, a filled zone) — and then with another semantic token. The halo tracks the PLOT ground, not the card. The shipped themes declare that mirror in the token layer (`--chart-background: var(--card)` in `themes.css`), so the two agree by contract, not by coincidence. A theme that paints the plot on something other than `--card` must set `--chart-background` to that ground, or every halo punches a card-coloured hole in it. |
haloWidth | number | Halo width in px (default 3). The visible halo is half of it — a stroke is centred. |
DrawPath
| Prop | Type | Description |
|---|---|---|
duration | number | Draw duration in seconds (default 0.9). |
delay | number | Delay before drawing starts, in seconds — pair it with `stagger(i, …)`. |
transition | Transition | Full transition override. Wins over `duration`/`delay`. |
ChartTooltipBox
| Prop | Type | Description |
|---|---|---|
xrequired | number | Anchor X in pixels, relative to the container — a crosshair, a mark centre. |
yrequired | number | Anchor Y in pixels, relative to the container. |
visiblerequired | boolean | Whether the tooltip is visible |
containerRefrequired | RefObject<HTMLDivElement | null> | The chart container: the box is portaled into it and placed around it. |
containerWidthrequired | number | Container width in the chart's own pixels (scales `x`/`avoid` to the screen). |
containerHeightrequired | number | Container height in the chart's own pixels. |
offset | number | Space kept between the box and the pointer or marks it avoids. Default 16. |
className | string | Custom class name |
childrenrequired | React.ReactNode | Tooltip content |
left | number | ReturnType<typeof useSpring> | @deprecated Bypasses placement: the box is positioned inside the container at this left, as before placement existed, and can cover the pointer. |
top | number | ReturnType<typeof useSpring> | @deprecated Bypasses placement: the box is positioned inside the container at this top, as before placement existed, and can cover the pointer. |
flipped | boolean | @deprecated Only read with `left`/`top`: the side the entrance slides from. |
springConfig | SpringConfig | Per-chart override; falls back to `ChartConfigProvider.tooltipBoxSpring`. |
panelStyle | React.CSSProperties | Inline styles for the inner tooltip panel. |
avoid | ChartTooltipAvoid | readonly ChartTooltipAvoid[] | The hovered mark(s) the box must not cover — a dot, a bar's band, a cell, a node. Small marks are stepped around; a mark too large to avoid (a big treemap leaf) is covered by at most half. The pointer and a keyboard-focused target are always avoided without being listed here. |
track | ChartTooltipTrack | How the anchor moves: `"x"` for a crosshair scrubbing along x (the box keeps its top at `y`), `"y"` for rows, `"free"` (default) for a mark anywhere in the plot. |
pinned | boolean | Freezes the placement while a touch tooltip is pinned (the finger has lifted). |
ChartDatapointLayer
| Prop | Type | Description |
|---|---|---|
label | string | Accessible name for the target group. Defaults to a localized generic name. |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| linkStroke | var(--chart-grid), 1.4px — links are furniture, not data, whatever the palette |
| nodeMono | --chart-mono-1 … 7, indexed by DEPTH (the default) — every node at one depth shares one shade |
| nodeCategorical | one hue per top-level branch; the root stays var(--chart-mono-4), since it belongs to no branch |
| label | HaloText in the chart-value type role; a closed branch reads 'Name (n)', n = its direct children |
| collapsed | a ring (r + 3, the node colour) around a closed branch's dot, plus the '(n)' count — shape and text, never colour alone; custom nodes get a bg-card pill with a chevron and '(n)' |
| motion | open/close, orientation and data changes animate over --duration-slow (ease-standard); nodes grow from and fold into their parent; reduced motion snaps |