Skip to main content

SankeyChart

Flow and Hierarchychart

Flow diagram — how quantity moves between stages or nodes.

import { SankeyChart } 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 SankeyChart 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 SankeyChart up before writing code: `npx -y @elabs-ai/components-cli docs SankeyChart` 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? Routes through stages.
  • a weighted flow between named nodes, source to target

Avoid

  • the nodes have no real flow between them — use a network chart
  • A Sankey for a simple two-column comparison — reach for a bar chart.
  • Raw colors on nodes/links — resolve the chart tokens so the dark theme reads correctly.
  • So many nodes that the labels collide — aggregate the tail into one 'other' node.

Works with

Contains

  • SankeyNode
  • SankeyLink
  • SankeyThreadLinks
  • SankeyTooltip
  • SankeyProvider

Examples

9 live examples

Default

With Accessible Label

`accessibleLabel` with no `accessibleDescription` (RM-184): the figure gets a generated description — node/link counts, the one fact an `aria-hidden` Sankey body withholds — through the same shared seam `LineChart`/`PieChart`/etc. use.

Controlled Hover

`hoveredIndex` / `onHoverChange` (RM-195, ADR 0042 A.5 rows 28–29): the host owns the hover state and drives the highlight through props, the same contract Pie/Ring/Radar/Legend already use. Hovering a node fires `onHoverChange` with its index; the `<output>` below proves the round trip actually reaches the host, not just that the callback was called.

Loading

`status="loading"` (RM-184): the skeleton + `ChartLoadingLabel`, until the data arrives.

Decorated

The funnel flow at decoration 10 (#255). Each palette node gains a series-pattern fill, so `SankeyNode` renders its `<pattern>` defs from a `.map()` — the second call site a missing list identity used to warn on. Pinned with `DecorationProvider` so a headless run reaches the branch.

Minimal

Pipeline

Threads

Palette

`palette="mono"` (RM-186): nodes, and the links that take their colours, cycle through five steps of the neutral ladder instead of `--chart-1`..`--chart-5`.

API

SankeyChart

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.
datarequiredSankeyDataSankey data with nodes and links
marginPartial<Margin>Chart margins
animationDurationnumberAnimation duration in milliseconds. Default: 1100
enterTransitionTransitionMotion enter transition (spring or cubic-bezier tween).
revealSignaturestringSignature of motion URL state — triggers enter replay when it changes.
aspectRatiostringAspect ratio as "width / height". Default: "2 / 1"
plotHeightResponsive<ChartPlotHeight>The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Wins over `aspectRatio`, which stays an alias.
nodeWidthnumberNode width in pixels. Default: 16
nodePaddingnumberNode padding in pixels. Default: 24
classNamestringAdditional class name for the container
childrenrequiredReactNodeChild components (SankeyNode, SankeyLink, SankeyTooltip)
hoveredIndexnumber | nullControlled hovered node index (e.g. from ChartLegend).
hoveredNodeIndexnumber | nullControlled hovered node index. @deprecated Since 6.0.0, use `hoveredIndex`. Removed in 7.0.0.
onHoverChange(index: number | null) => voidCalled when node hover changes from the chart surface.
onNodeHoverChange(index: number | null) => voidCalled when node hover changes from the chart surface. @deprecated Since 6.0.0, use `onHoverChange`. Removed in 7.0.0.
modeSankeyModeRendering mode (RM-037). `"aggregate"` (default) is today's behavior — one edge per node pair via `d3-sankey`, rendered with `SankeyLink`. `"threads"` additionally accepts a `path: string[]` (ordered node names) on each `data.links` record and renders one polyline per record via `SankeyThreadLinks` — node positions come from the SAME aggregate layout, so switching modes never moves a node. Omitting `mode` (or passing `"aggregate"`) is byte-identical to pre-RM-037 output.
accessibleLabelChartA11yProps["accessibleLabel"]Accessible name for the chart region. Combine with a generated description (RM-184): setting this with no `accessibleDescription` gets an auto summary — `"Sankey diagram, 5 nodes, 8 links"` — the counts an `aria-hidden` Sankey body withholds, the same shared seam `LineChart`, `AreaChart`, `BarChart`, `ScatterChart` and `PieChart` already use.
accessibleDescriptionChartA11yProps["accessibleDescription"]Supplemental description read by AT. Overrides the generated summary above.
statusChartStatusShow the loading skeleton until the data is ready (ADR 0042 §9). Default `"ready"`.
emptyChartEmptyStateTitle and message shown when there is nothing to plot.
paletteChartPaletteColour ramp for the nodes (RM-186): five colours through `resolvePalette`, cycled; links and threads follow their source node. A `SankeyNode` `fill` / `getNodeColor` still wins. Unset: `--chart-1` … `--chart-5`, as before.
PropTypeDescription
strokestringStroke color for links (overrides gradient). Default: uses gradient
strokeOpacitynumberStroke opacity. Default: 0.5
fadedOpacitynumberOpacity when another link/node is hovered. Default: 0.1
useGradientbooleanUse gradient from source to target color. Default: true
getNodeColor(node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>, index: number) => stringCustom function to get node color (for gradient)
getLinkColor(link: SankeyLinkType<SankeyNodeDatum, SankeyLinkDatum>, index: number) => stringCustom link color function (overrides gradient)
patternsReact.ReactNodePattern definitions to render in defs. Use @visx/pattern components (PatternLines, PatternCircles, etc.)
getLinkPattern( link: SankeyLinkType<SankeyNodeDatum, SankeyLinkDatum>, index: number, ) => string | null | undefinedReturn pattern ID for a link, or null/undefined to use gradient/solid color

SankeyNode

PropTypeDescription
fillstringFill color for nodes. Default: uses theme colors
lineCapnumberCorner radius for nodes. Default: 4
fadedOpacitynumberOpacity when another node/link is hovered. Default: 0.4
showLabelsbooleanShow node labels. Default: true
getNodeColor(node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>, index: number) => stringCustom node color function
formatValue(value: number) => stringThe value line under each node's name, from the node's flow. Default: `"<n> sessions"` (the unit this chart shipped with). Pass your own to name the unit the links actually count — `(v) => \`${v} invoices\``. The same text is what the label-fit measurement reserves room for.
PropTypeDescription
getThreadColor( route: number[], nodes: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>[], ) => stringStroke color per thread. Default: the route's source node's palette color.
fadedOpacitynumberOpacity applied to every OTHER thread while one is hovered, focused or pinned. Default: 0.035
hitTwinWidthnumberFat hit-twin stroke width in px (the invisible pointer hit area). Default: 9
classNamestringAdditional class name for the thread group.

SankeyTooltip

PropTypeDescription
nodeContent(props: { node: SankeyNode<SankeyNodeDatum, SankeyLinkDatum>; index: number; }) => React.ReactNodeCustom content renderer for node tooltips
linkContent(props: { link: SankeyLink<SankeyNodeDatum, SankeyLinkDatum>; index: number; }) => React.ReactNodeCustom content renderer for link tooltips
formatValue(value: number) => stringValue formatter function
classNamestringCustom class name

Theming

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

StateToken
series--chart-1 … --chart-5
modeaggregate (default) | threads