Sankey Flow
Sankey diagram: automatic column layout, link widths proportional to value, animated gradient flows, and hover that traces a full path while dimming the rest — self-contained layout, no d3
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
Draw where a quantity comes from and where it goes: traffic sources → landing pages → conversions, budget allocation, energy or money flows. Nodes are placed into columns automatically from the link topology, node heights and link widths are proportional to value, and each link is a gradient from its source color to its target color with a slow, flowing dash animation. Hover any link and its whole upstream and downstream path stays lit while everything else fades to 15%, so you can see at a glance where a source ends up. The layout algorithm is a self-written, simplified one with no d3 dependency.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/sankey-flow.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SankeyFlow/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/sankey-flow.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/sankey-flow.
Usage
import {
SankeyFlow,
type SankeyLink,
type SankeyNode,
} from "@/components/ui/sankey-flow";
const nodes: SankeyNode[] = [
{ id: "search", label: "搜尋" },
{ id: "social", label: "社群" },
{ id: "home", label: "首頁" },
{ id: "campaign", label: "活動頁" },
{ id: "purchase", label: "完成購買", color: "#22c55e" },
{ id: "leave", label: "未購買", color: "#f43f5e" },
];
const links: SankeyLink[] = [
{ source: "search", target: "home", value: 420 },
{ source: "search", target: "campaign", value: 180 },
{ source: "social", target: "campaign", value: 260 },
{ source: "home", target: "purchase", value: 120 },
{ source: "home", target: "leave", value: 300 },
{ source: "campaign", target: "purchase", value: 210 },
{ source: "campaign", target: "leave", value: 230 },
];
<SankeyFlow
nodes={nodes}
links={links}
valueFormatter={(v) => `${v.toLocaleString()} 人`}
onLinkClick={(link) => console.log(link.source, "→", link.target)}
/>;Node labels above: 搜尋 (Search), 社群 (Social), 首頁 (Home page), 活動頁 (Campaign page), 完成購買 (Purchased), 未購買 (Did not purchase); 人 means "people".
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | SankeyNode[] | — | Node list; links refer to nodes by id, and the order sets the default colors |
links | SankeyLink[] | — | Flow list; links that would create a cycle or point at a missing node are skipped with a console warning |
height | number | 360 | SVG viewBox height (px); the width is fixed at 720 and the chart scales with its container |
nodeWidth | number | 14 | Node rectangle width (px) |
nodePadding | number | 12 | Vertical gap between nodes in the same column (px) |
valueFormatter | (value: number) => string | — | Value formatter used by the tooltip, node labels for assistive tech, and the screen reader list; defaults to thousands separators |
align | "justify" | "left" | "justify" | Column alignment: justify pushes terminal nodes (no outflow) to the last column, left places nodes by topological depth |
onNodeClick | (node: SankeyNode) => void | — | Called when a node is clicked (or activated with Enter / Space while focused) |
onLinkClick | (link: SankeyLink) => void | — | Called when a link is clicked |
className | string | — | Passed through to the outer container |
SankeyNode
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique node id that links refer to |
label | string | — | Label text shown beside the node |
color | string | — | Node color (also the link gradient end color); cycles through six built-in colors by node order when omitted |
SankeyLink
| Field | Type | Default | Description |
|---|---|---|---|
source | string | — | Source node id |
target | string | — | Target node id |
value | number | — | Flow value that sets the link width (proportional, minimum 1.5px); must be greater than 0 |
The SankeyNode, SankeyLink, and SankeyFlowProps types are also exported by name.
How it works
- A simplified, self-written layout — not d3-sankey: node columns come from the link topology (the longest path from the source side); nodes in a column are stacked top to bottom by descending total value and centered vertically. A node's height is the larger of its total inflow and total outflow, and the whole chart shares one scale so values are comparable across columns. There is no iterative relaxation like d3-sankey, so complex graphs can show more crossings
- Link endpoints: the links on a node are ordered by the vertical position of the node on the other side, then slice the node's height by value to reduce crossings; each path is a cubic Bézier S-curve with both control points at the horizontal midpoint, so links leave and enter nodes horizontally
- Cycle guard: a Sankey diagram must be a directed acyclic graph. The component checks
linksone by one in order; a link that would close a cycle (or loops onto itself, points at a missing node, or has a value of 0 or less) is skipped and reported withconsole.warn - Hover tracing: hovering a link recursively collects its whole upstream and downstream path, which stays highlighted while everything else drops to 15%; hovering a node lights only its incoming and outgoing links. The tooltip shows "source → target" and the value
- Label placement: labels sit to the right of nodes in the left half of the columns and to the left in the right half, so text in the outermost columns is never clipped
Accessibility
- The SVG is
role="img"with anaria-labelstating the number of nodes and flows; ansr-onlylist also spells out every flow as "source 流向 (flows to) target: value", so screen readers get the full data - Every node is reachable with Tab; focusing a node highlights its flows just like hover, and each node carries an
aria-label(name and total value) and afocus-visibleoutline; Enter or Space triggersonNodeClick(Enter during IME composition is ignored) - The tooltip is a purely visual aid (
aria-hidden); the same information is provided by thesr-onlylist - When the user has "reduce motion" enabled at the system level, the node fade-in and link draw-in entrances are disabled and the flowing dash loop is not shown — the chart renders directly in its final, static state
Funnel Chart
An SVG conversion funnel React component: trapezoid stages grow in one after another, stage-to-stage conversion labels sit between layers, hovering highlights a stage with a tooltip, in vertical and horizontal modes with a screen reader data table.
Route Map Card
A navigation-style route card — a grid basemap with deterministic streets, a route drawn segment by segment, a marker travelling along it, street labels fading in on arrival and a live ETA readout.