DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChartArea ChartBar ChartLine ChartLive Line ChartComposed ChartPie ChartFunnel ChartRadar ChartRadial ChartSankey ChartScatter ChartCandlestick ChartSunburst ChartChoropleth ChartActivity Heatmap
AI agentsContributing
  1. Components
  2. Sankey Chart

Sankey Chart

Token-driven flow chart built on Recharts v3 — weighted links between nodes in columns, for where a total comes from and where it goes.

$ pnpm add @tessinaui/ui

When to use

  • To show where a total comes from and where it goes. Income split into spending, revenue into costs and profit, energy from its sources to its uses. Each band is as wide as the amount it carries.
  • To show a journey with its drop-offs. A checkout, a sign-up or a product tour: each step is a column, and the people who left at a step are a band of their own (Journey and drop-off).
  • To show how groups mix on the way through. Traffic from four channels landing on three pages, then ending in a sign-up or not.

When not to use

  • For one step of shares. A total split once, with nothing before or after it, is a PieChart or a BarChart.
  • For a trend over time. Use LineChart or AreaChart.
  • For a flow that loops back. A Sankey reads in one direction. A link that returns to an earlier node cannot be drawn, and the chart shows its error state instead (States).
  • For more than about twenty nodes. Bands get thin, labels drop to the tooltip, and the keyboard walk gets long. Fold the small ones into "Other" first (Group the tail).
  • When the exact values matter more than the shape. Put the flows in a Table and keep the chart as the summary.

Installation

Terminal
pnpm add @tessinaui/ui recharts

Usage

example.tsx
import { SankeyChart } from "@tessinaui/ui";
import type { SankeyChartData } from "@tessinaui/ui";

// Nodes by id; links name their ends by that id.
const data: SankeyChartData = {
  nodes: [
    { id: "salary", label: "Salary" },
    { id: "income", label: "Income" },
    { id: "rent", label: "Rent" },
    { id: "saved", label: "Saved" },
  ],
  links: [
    { source: "salary", target: "income", value: 4000 },
    { source: "income", target: "rent", value: 1600 },
    { source: "income", target: "saved", value: 2400 },
  ],
};

<SankeyChart data={data} label="Six tenths of the salary is saved" />

Examples

Default

One hue for every node and a band for every flow. Point at a node to see its flows in and out, or at a band for its share of where it came from. The rest step back while you do.

Node colours

Colour a node through config. Give colour to the nodes a reader has to tell apart, here the four channels, and leave the rest in the chart's one hue. There are six chart colours, and a flow often has more nodes than that.

Link colour

linkColor paints each band with its source node's colour, its target's, a gradient from one to the other, or neutral, leaving every colour to the nodes. The default is source.

Alignment and order

align="justify", the default, puts every node with nothing going out in the last column. align="left" keeps each node one column past what feeds it, so a flow that ends early ends where it stops. sort orders each column by value, largest first; sort={false} keeps the order of your data.

Nodes

nodeWidth and nodePadding set a node's thickness and the space between nodes, linkCurvature how far a band bends, and rounded the node's corners.

Labels

labels prints a node's name, its value under the name (the default), its percent of the total, both, or none. The total is everything that flows out of the first column. A label that does not fit beside its node is left to the tooltip, so labels never print over each other.

Icons

An icon in a node's config entry draws before its name, and in the legend.

Journey and drop-off

columns puts a heading over each column, for the steps of a journey. The people who leave at a step are a node of their own. Give those nodes a neutral tone, not an error colour: leaving is part of a funnel, not a fault.

Path highlight

Pointing at or focusing a node dims everything more than one hop away. A journey is read by path, so highlight="path" keeps every flow through the node at full strength instead, back to where it came from and on to where it ended. Point at a channel to follow it to payment, or at "Paid" to see every route that got there. A band keeps what fed its source and what its target went on to.

Choosing a node

With selectionMode="single" a click, a tap, or Enter or Space on a focused node chooses it, and choosing it again clears it. The chosen node wears a ring; the nodes off its flows step back to a wash inside an outline, and their bands fade, the same set a hover lights (a path under highlight="path"). A band is not a choice: clicking one chooses nothing. Here a card beside the chart lists the chosen node's flows in and out, and its × clears the choice.

Tooltip and formatting

valueFormatter spells every number the chart prints, in the labels and the tooltip. A node's tooltip lists its total and every flow in and out of it. A band's tooltip gives its value and its share of its source.

Legend

showLegend lists the nodes that carry a colour, in config order. It is off by default because every node is already named beside it; turn it on when the colours group nodes, as the three admission routes do here.

Group the tail

Fold the small flows into one "Other" node before passing the data. Nine categories would be nine thin bands and nine labels; four with "Other" read at a glance.

In a card

The heading, the totals and any controls belong to the card. The chart draws the flow.

Sizes

size sets the aspect ratio: sm 3:1, md 2:1, lg 16:9. Each has a minimum height, so the columns keep room to separate in a narrow container.

Narrow containers

Below 480px the chart prints names only, inside the columns, and the figures move to the tooltip. A flow with more columns than fit at 112px each keeps that width and scrolls sideways. Moving focus to a node scrolls it into view.

Accessible name and data table

Name the chart with its insight, put the numbers in description, and offer the flows as a table when someone may need the exact values.

On a coloured plate

Inside a coloured Surface a node with no colour takes the plate's ink, the bands a wash of it, and the labels its text tiers. No prop is needed.

Right-to-left

The flow runs left to right in every direction, as the reading of time does. With dir="rtl" the words the chart writes are Arabic, and the tooltip and the names it reads out follow the page.

States

loading draws a skeleton in the chart's shape, error shows your message, and a flow with nothing to draw shows emptyState. A flow that loops back on itself shows the error state too, and a development warning names the link that closes the loop.

API Reference

PropTypeDefaultDescription
data{ nodes: { id, label? }[]; links: { source, target, value }[] }—The flow. Links name their ends by node id. A link of zero or less is not drawn, and a node no link touches is left out.
configChartConfig—Node metadata keyed by node id: label, color or theme, icon.
linkColor"source" | "target" | "gradient" | "neutral""source"What a band is painted with.
align"justify" | "left""justify"Where a node with nothing going out sits.
sortbooleantrueOrder each column by value. false keeps data order.
nodeWidthnumber12Node thickness, in px.
nodePaddingnumber16Space between nodes in a column, in px.
linkCurvaturenumber0.5How far a band bends, 0 to 1.
labels"name" | "value" | "percent" | "value-percent" | "none""value"What prints beside each node.
columnsstring[]—A heading over each column, in order.
highlight"adjacent" | "path""adjacent"What stays at full strength while a node or band is active: one hop, or every path through it.
selectionMode"none" | "single""none""single" lets a reader choose one node by click, tap, Enter or Space; choosing it again clears it. Links do not select.
selectedstring | null—The chosen node's id, controlled. null chooses nothing.
defaultSelectedstring | nullnullThe node chosen at first render, uncontrolled.
onSelectedChange(id, { datum, index, source }) => void—Called on every choice and clear; datum is the node from data.nodes, null on a clear.
activestring | null—The node lit as if pointed at, controlled — to light a node from a list beside the chart.
onActiveChange(id: string | null) => void—The node under the chart's own pointer or focus; a band reports null.
valueFormatter(value: number) => stringIntl.NumberFormatFormats every value the chart prints.
rounded"none" | "sm" | "md" | "lg" | "full""md"Node corners.
size"sm" | "md" | "lg""md"Aspect ratio.
showLegendbooleanfalseA key for the nodes that carry a colour.
showTooltipbooleantrueShow the tooltip on point and focus.
stringsPartial<SankeyChartStrings>follows dirEvery word the chart writes, per entry.
childrenReactNode—Extra Recharts children, drawn inside the chart's svg.
dir"ltr" | "rtl"—Picks the chart's words. The flow runs left to right either way.
labelstring—Accessible name; renders the chart as a role="figure".
descriptionstring—Visually hidden summary for assistive tech.
loadingbooleanfalseRender the skeleton.
errorReactNode—Render an error message instead of the chart.
emptyStateReactNode"No data to display"Shown when no link can be drawn.

Colour

A node with no colour in config takes --chart-1, so one hue carries the whole flow and colour stays free for the nodes that need it. There are six chart colours; when more nodes need telling apart, group them, fold the small ones into "Other", or let the labels name them.

Colour is never the only thing that names a node. Every node is labelled beside it or, where the label does not fit, in its tooltip and its accessible name.

Do not colour a node with a success or error token. A chart colour is a category, not a verdict. A drop-off node takes a neutral tone such as hsl(var(--muted-foreground)).

Accessibility

  • Give every chart a label. State the insight: "Housing and savings take more than half the month", not "Sankey chart of the budget".
  • Put the numbers in description. A static read should carry the message without anyone stepping through the nodes.
  • Keyboard. Tab moves focus to the first node and draws the focus ring around it. The Up and Down arrow keys move within a column; Left and Right move to the nearest node in the next column; Home and End go to the first and the last node. The chart is one stop in the tab order. Each node is read as its name, value, share of the total and how many flows go in and out, and its tooltip lists every one of those flows, so each band's value is reachable from the keyboard. Escape hides the tooltip and keeps focus. With selectionMode="single", Enter or Space chooses the focused node or clears it; Escape never clears a choice.
  • Screen readers. The nodes are a list, or a list box of options that say whether they are selected when the chart selects; a choice and a clear are announced once. The printed labels are hidden from assistive tech, because each node's name already says them in full, where the printed line may be cut to fit.
  • Pointer. Every node and every band has a target wider than its mark: 24px with a mouse, 44px under a finger.
  • Motion. Nothing animates in. The stepping back of the other nodes and bands is a fade, and under prefers-reduced-motion it is instant. A choice changes state at once: its ring and the step-back move nothing.
  • Not yet verified with VoiceOver, NVDA or JAWS, which is why the component is in beta.

Composition

children are drawn inside the chart's svg, so Recharts' own parts still compose. The chart draws its own tooltip and legend; do not add Recharts' Tooltip or Legend beside them.

Boundaries

  • No loops. A flow that returns to an earlier node is reported, not drawn.
  • No vertical layout. The flow runs left to right. On a phone it prints names only and, past its column floor, scrolls.
  • No click or selection yet. A node shows its flows on hover and focus; it is not a control.
  • No highlight of a whole path. Pointing at a node keeps its own flows and their ends at full strength, one step each way.

Theming

Node colours come from config, as --chart-1 to --chart-6 or any colour you pass; both themes are handled by the tokens. Labels, column headings, the legend and the tooltip use the page's text, border and surface tokens, and follow a coloured Surface without a prop.

Radial ChartScatter Chart

On this page

When to useWhen not to useInstallationUsageExamplesDefaultNode coloursLink colourAlignment and orderNodesLabelsIconsJourney and drop-offPath highlightChoosing a nodeTooltip and formattingLegendGroup the tailIn a cardSizesNarrow containersAccessible name and data tableOn a coloured plateRight-to-leftStatesAPI ReferenceColourAccessibilityCompositionBoundariesTheming