Skip to main content

TreeChart

Flow and Hierarchychart

Fixed-spacing hierarchy diagram, left to right or top to bottom, drawn as dots or as your own same-size cards, whose branches open and close by default — no values, no sizes: ‘who belongs to whom’.

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

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 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

Contains

  • HaloText
  • DrawPath
  • ChartTooltipBox
  • ChartDatapointLayer

Pairs with

Examples

13 live 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`.

`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

PropTypeDescription
messagesChartMessagesmessages 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.
datarequiredTreeNode<TData>The hierarchy. Nodes need no `value`: membership only.
orientationTreeOrientation`"lr"` (default) or `"tb"`. Both work with default and custom nodes.
nodeSizenumberNode dot diameter, in px. Default `7`. Ignored with `renderNode`.
paletteTreePaletteWhich colour family shades a node. Default `"mono"`.
collapsiblebooleanBranches 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`.
expandedIdsstring[]Open branches, by node id (controlled). Pair with `onExpandedChange`.
defaultExpandedIdsstring[]Open branches on first render (uncontrolled). Wins over `defaultExpandedDepth`.
onExpandedChange(ids: string[]) => voidFires with the full list of open branch ids, in depth-first order, after every open or close.
defaultExpandedDepthnumberUncontrolled 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.
collapseDepthnumber@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>) => ReactNodeDraws 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>) => ReactNodeDraws 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`.
zoombooleanA 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.
zoomablebooleanA 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).
defaultZoomnumberThe zoom on first render with `zoom`. Default `1`.
onZoomChange(zoom: number) => voidFires after every zoom change (buttons, wheel, pinch, fit).
minimapbooleanA 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`.
nodeWidthnumberCustom node box width, px. Default `160`. Only used with `renderNode`.
nodeHeightnumberCustom 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.
classNamestring
plotHeightResponsive<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).
accessibleLabelChartA11yProps["accessibleLabel"]Accessible name for the chart region (and its tree).
accessibleDescriptionChartA11yProps["accessibleDescription"]Supplemental description read by AT.
statusChartStatusShow the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`.
onDatapointClickChartDatapointClickHandler<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.
copyValueOnActivatebooleanPut 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.
datapointLabelChartDatapointLabel<Record<string, unknown>>Override the accessible name of each keyboard target.
maxInteractiveDatapointsnumberDev-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.

Also accepts: ChartInteractionProps

TreemapChart

PropTypeDescription
messagesChartMessagesmessages 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.
datarequiredTreemapNodeThe hierarchy. A leaf needs a `value`; a parent's explicit `value` (if any) must equal the sum of its children (dev-validated — see `validateTreemapData`).
depth1 | 2How many levels of rectangles to render. `1` = flat (root's children only, no title bands). `2` = groups + leaves. Default `2`.
paletteTreemapPalette- `"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"`).
gapnumberPaper seam between tiles, in px. Default `2`.
labelMinAreanumberHide 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) => booleanSkip 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.
monoLeafColorstringOverride 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.
monoBandColorstringOverride for the `depth: 2` group title band's fill — same reasoning as {@link monoLeafColor}.
otherThresholdnumberMerge leaves under this share (0..1) of their parent's total into "Other". `0` (default) = off — the unconditional 30-leaf cap still applies.
drilldownbooleanWhen `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`.
showValuesboolean@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.
labelsboolean | ChartDataLabelsConfigPrint 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.
valueFormatChartValueFormatHow leaf values render in the tooltip and (with `labels`) on tiles. Default `"compact"`.
classNamestring
styleCSSProperties
aspectRatiostringAspect ratio as "width / height". Default `"16 / 9"`.
plotHeightResponsive<ChartPlotHeight>The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Wins over `aspectRatio`, which stays an alias.
accessibleLabelChartA11yProps["accessibleLabel"]Accessible name for the chart region (announces to AT on focus).
accessibleDescriptionChartA11yProps["accessibleDescription"]Supplemental description read by AT.
legendContainerLegendPropRenders 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).
statusChartStatusShow the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`.
emptyChartEmptyStateTitle and message shown when there is nothing to plot.
selectionStatesChartSelectionStatesResolver<Record<string, unknown>>Maps a mark (category, optional series key, optional raw row) to its selection state. Unset → the chart renders exactly as before.
dimExcludedbooleanDim `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.
onDatapointClickChartDatapointClickHandler<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.
copyValueOnActivatebooleanPut 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.
datapointLabelChartDatapointLabel<Record<string, unknown>>Override the accessible name of each keyboard target.
maxInteractiveDatapointsnumberDev-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.

Also accepts: ChartSelectionProps, ChartInteractionProps

HaloText

PropTypeDescription
halostringThe 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.
haloWidthnumberHalo width in px (default 3). The visible halo is half of it — a stroke is centred.

Also accepts: SVGProps<SVGTextElement>

DrawPath

PropTypeDescription
durationnumberDraw duration in seconds (default 0.9).
delaynumberDelay before drawing starts, in seconds — pair it with `stagger(i, …)`.
transitionTransitionFull transition override. Wins over `duration`/`delay`.

Also accepts: Omit< SVGProps<SVGPathElement>, | "onAnimationStart" | "onAnimationEnd" | "onAnimationIteration" | "onDrag" | "onDragEnd" | "onDragEnter" | "onDragExit" | "onDragLeave" | "onDragOver" | "onDragStart" | "onDrop" | "pathLength" | "strokeDasharray" | "strokeDashoffset" | "style" | "values" >

ChartTooltipBox

PropTypeDescription
xrequirednumberAnchor X in pixels, relative to the container — a crosshair, a mark centre.
yrequirednumberAnchor Y in pixels, relative to the container.
visiblerequiredbooleanWhether the tooltip is visible
containerRefrequiredRefObject<HTMLDivElement | null>The chart container: the box is portaled into it and placed around it.
containerWidthrequirednumberContainer width in the chart's own pixels (scales `x`/`avoid` to the screen).
containerHeightrequirednumberContainer height in the chart's own pixels.
offsetnumberSpace kept between the box and the pointer or marks it avoids. Default 16.
classNamestringCustom class name
childrenrequiredReact.ReactNodeTooltip content
leftnumber | 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.
topnumber | 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.
flippedboolean@deprecated Only read with `left`/`top`: the side the entrance slides from.
springConfigSpringConfigPer-chart override; falls back to `ChartConfigProvider.tooltipBoxSpring`.
panelStyleReact.CSSPropertiesInline styles for the inner tooltip panel.
avoidChartTooltipAvoid | 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.
trackChartTooltipTrackHow 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.
pinnedbooleanFreezes the placement while a touch tooltip is pinned (the finger has lifted).

ChartDatapointLayer

PropTypeDescription
labelstringAccessible name for the target group. Defaults to a localized generic name.

Also accepts: HTMLAttributes<HTMLDivElement>

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
linkStrokevar(--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
nodeCategoricalone hue per top-level branch; the root stays var(--chart-mono-4), since it belongs to no branch
labelHaloText in the chart-value type role; a closed branch reads 'Name (n)', n = its direct children
collapseda 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)'
motionopen/close, orientation and data changes animate over --duration-slow (ease-standard); nodes grow from and fold into their parent; reduced motion snaps