Skip to content

shadcn.io is an independent project built on shadcn/ui

Free

Sankey diagram

Where traffic, budget or users go from one stage to the next, each link as wide as its value. Hover a node or link to keep only its paths lit.

Charting library: TanStack Charts

Preview

Preview width

Installation

The command adds the example above to components/charts/ and, the first time, the chart kit to components/ui/charts/. Both are source files in your project from then on: read them, change them, delete what you don't use.

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Usage

data has nodes, each with a name, and links that point at nodes by index with a value. Links with a value of zero or less are left out.

<script setup lang="ts">
import {
  SankeyChart,
  type SankeyData,
  SankeyLink,
  SankeyNode,
  SankeyTooltip,
} from "@/components/ui/charts";

const data: SankeyData = {
  nodes: [
    { name: "Search" },
    { name: "Social" },
    { name: "Pricing" },
    { name: "Docs" },
    { name: "Signed up" },
    { name: "Left" },
  ],
  links: [
    { source: 0, target: 2, value: 3200 },
    { source: 0, target: 3, value: 1800 },
    { source: 1, target: 2, value: 1400 },
    { source: 1, target: 3, value: 600 },
    { source: 2, target: 4, value: 1900 },
    { source: 2, target: 5, value: 2700 },
    { source: 3, target: 4, value: 900 },
    { source: 3, target: 5, value: 1500 },
  ],
};

const margin = { left: 100, right: 100 };

const formatVisits = (value: number) =>
  `${value.toLocaleString("en-US")} visits`;
</script>

<template>
  <SankeyChart
    ariaLabel="Visits from source to sign-up"
    :data="data"
    :margin="margin"
  >
    <SankeyLink />
    <SankeyNode :format-value="formatVisits" />
    <SankeyTooltip value-label="Visits" />
  </SankeyChart>
</template>

Anatomy

SankeyNode, SankeyLink and SankeyTooltip render nothing themselves: they configure the chart. Leave one out and its part keeps its defaults.

<SankeyChart :data="data" :margin="margin">
  <SankeyLink :stroke-opacity="0.6" />
  <SankeyNode :format-value="formatVisits" />
  <SankeyTooltip value-label="Visits" />
</SankeyChart>
  • SankeyChart: Lays out the nodes and links with d3-sankey and draws them.
  • SankeyNode: The node bars and their labels: color, corners, label direction and value format.
  • SankeyLink: The links: a gradient from source to target by default, or one color, or a pattern.
  • SankeyTooltip: The panel for a hovered node or link, the default one or yours.

Colors

Nodes take --chart-1 to --chart-5 in order, and each link fades from its source's color to its target's. To color by something else, such as the stage, pass the same getNodeColor to SankeyNode and SankeyLink.

:useGradient="false" paints each link in the color of the node it leaves. stroke paints them all one color, and getLinkColor picks one per link.

Preview width

Labels

Names sit beside the nodes with each value underneath. labelOrientation="vertical" turns them to run along the node, which needs less room at the sides. :showLabels="false" hides them for a compact chart you read by hovering.

Values read "N sessions" until you pass formatValue to SankeyNode. A node's value is the larger of what flows in and what flows out.

Preview width

Patterned links

Put SVG patterns in SankeyLink's patterns, a PatternPreset or your own <pattern>, and return a pattern's id from getLinkPattern. A link that gets null keeps its color.

Preview width

Driving the hover

hoveredNodeIndex sets the hovered node from outside the chart, and onNodeHoverChange reports the chart's own. Keep one index in state and a row of buttons can light everything a node feeds.

Preview width

Customization

The kit is in your project, in components/ui/charts/: your framework's components over a shared core/. Change a default there and every chart picks it up.

class goes on the chart's container, so size and spacing are utility classes.

SankeyTooltip takes nodeContent and linkContent. Each gets the hovered item and its index, and what it returns fills the panel. For the default panel, formatValue and valueLabel change the number and its label.

The default margin leaves 180 px on each side for labels, which is a lot on a small card. Pass your own, as every example does, and nodeWidth and nodePadding for the bars.

<SankeyTooltip>
  <template #link-content="{ link }">
    <div class="grid gap-1">
      <span class="font-medium">
        {{ data.nodes[link.source]?.name }} → {{ data.nodes[link.target]?.name }}
      </span>
      <span class="text-muted-foreground">
        {{ link.value.toLocaleString("en-US") }} visits
      </span>
    </div>
  </template>
</SankeyTooltip>

Where a part renders content of its own, you pass it in. In Vue:

  • SankeyTooltip: <template #link-content="{ link, index }">
  • SankeyTooltip: <template #node-content="{ node, index }">

Loading state

The Sankey diagram has no loading state of its own. Until the data arrives, render a Skeleton with the chart's aspectRatio, 2 / 1 unless you set one, so the card keeps its height.

Animation

Nodes grow in one after another while their labels slide in from the side. A beat later the links draw left to right, staggered across the flows. Each part runs for animationDuration, 1100 ms by default.

enterTransition takes a spring or a cubic-bezier tween, in seconds, in place of the default curve. Change revealSignature to play the entrance again.

Theming

Every color in the kit is one of your theme's tokens or a mix of them, except --chart-positive, a fixed green. A preset or the .dark class restyles the chart, and overriding a token in your CSS changes it everywhere.

Links are drawn at strokeOpacity 0.5, a lighter version of their nodes' colors, and drop to 0.1 while something else is hovered. Node names use your --foreground.

TokenDefaultPaints
--chart-1your theme'sthe first series
--chart-2your theme'sthe second series
--chart-3your theme'sthe third series
--chart-4your theme'sthe fourth series
--chart-5your theme'sthe fifth series
--chart-tooltip-background--popoverthe tooltip panel
--chart-tooltip-muted--muted-foregroundthe tooltip's labels
globals.css
:root {
  --chart-tooltip-background: var(--card);
}

Accessibility

The chart is an image named by ariaLabel ("Sankey chart" by default). The widths carry the numbers, so say the main flow in ariaDescription: "Two thirds of search traffic reaches pricing."

The keyboard visits the nodes column by column, top to bottom, then the links in the same order. Each one shows its tooltip and lights what it connects to, as on hover.

Tab
Moves focus to the chart.
Arrow Right
Moves to the next node or link. ArrowDown does the same.
Arrow Left
Moves to the previous node or link. ArrowUp does the same.
Home
Jumps to the first node.
End
Jumps to the last link.

API reference

Vue takes the same props as React, in camelCase or kebab-case, with class for className. Render props are scoped slots (#content), and callbacks such as onHoverChange can be written as listeners (@hover-change).

SankeyChart

PropType
dataSankeyData

Nodes and links to draw. Links reference nodes by index; links with a non-positive or non-finite value are ignored, and the chart is empty when no positive link remains.

margin?Partial<Margin>

Chart margins in pixels. Any side you omit keeps its default; the wide left and right defaults leave room for node labels.

Type
Partial<Margin>
Default
{ top: 40, right: 180, bottom: 40, left: 180 }
animationDuration?number

Duration of the enter animation in milliseconds.

Default
1100
enterTransition?Transition

Enter transition (a spring or a cubic-bezier tween), in seconds. Overrides animationDuration when it sets a duration.

Default
a 1.1s tween on the reveal curve
revealSignature?string

Changing this value replays the enter animation.

Default
""
aspectRatio?string

Aspect ratio of the chart as "width / height".

Default
"2 / 1"
nodeWidth?number

Width of the node bars in pixels.

Default
16
nodePadding?number

Vertical gap between nodes of the same column in pixels.

Default
24
class?string

Additional class name for the chart container.

children?ReactNode

SankeyNode, SankeyLink and SankeyTooltip to configure the chart, plus any elements that call useSankey().

hoveredNodeIndex?number | null

Controlled hovered node index, for example from a legend. Overrides hover from the chart surface; null clears it.

Type
number | null
onNodeHoverChange?(index: number | null) => void

Called with the hovered node index (or null) when hover changes on the chart surface.

Type
(index: number | null) => void
ariaLabel?string

Accessible label of the chart.

Default
"Sankey chart"
ariaDescription?string

Longer accessible description of the chart.

SankeyNode

PropType
fill?string

Fill color of the nodes.

Default
the theme chart colors, by node
lineCap?number

Corner radius of the nodes in pixels.

Default
4
fadedOpacity?number

Opacity of nodes not connected to the hovered node or link.

Default
0.4
showLabels?boolean

Show node names.

Default
true
showValueLabels?boolean

Show node values under the names.

Default
true
formatValue?(value: number) => string

Formats the value shown under a node's name. A node's value is the larger of its inflow and outflow.

Type
(value: number) => string
Default
the value as an integer followed by " sessions"
labelOrientation?SankeyLabelOrientation

Reading direction of the labels outside the nodes.

Default
"horizontal"
getNodeColor?(node: SankeyPropNode, index: number) => string

Returns the color of a node. Overrides fill.

Type
(node: SankeyPropNode, index: number) => string
PropType
stroke?string

Solid color for all links. Overrides the gradient.

strokeOpacity?number

Opacity of the links.

Default
0.5
fadedOpacity?number

Opacity of links not connected to the hovered node or link.

Default
0.1
useGradient?boolean

Paint each link with a gradient from its source color to its target color.

Default
true
getNodeColor?(node: SankeyPropNode, index: number) => string

Returns the color of a node, used for the gradient ends.

Type
(node: SankeyPropNode, index: number) => string
getLinkColor?(link: SankeyPropLink, index: number) => string

Returns a solid color per link. Overrides the gradient.

Type
(link: SankeyPropLink, index: number) => string
#patterns?slot

Pattern definitions (SVG <pattern> elements) that links can reference by id through getLinkPattern.

getLinkPattern?(link: SankeyPropLink, index: number) => string | null | undefined

Returns the id of a pattern to fill a link with, or null/undefined to use the gradient or solid color. A pattern wins over stroke and getLinkColor.

Type
(link: SankeyPropLink, index: number) => string | null | undefined

SankeyTooltip

PropType
#node-content?slot: { node, index }

Custom tooltip content for a node. Receives the node and its index.

#link-content?slot: { link, index }

Custom tooltip content for a link. Receives the link and its index.

formatValue?(value: number) => string

Formats the values in the default tooltip.

Type
(value: number) => string
Default
integer formatting
valueLabel?string

Label of the value row in the default node tooltip.

Default
"Sessions"
class?string

Additional class name for the tooltip.

panelStyle?CSSProperties

Inline styles merged onto the tooltip panel, for example a background or a backdrop blur. A number is in pixels where CSS needs a unit.

exit?boolean

Fade the panel out when the hover ends (over the hover profile); false hides it at once.

Default
true

Examples

9 examples on Vue, each live with its code and its own install command.