Skip to main content
Urbicon UI

Sankey

Flow diagram for multi-stage data: nodes split into layers, and links are sized to the values that flow between them.

Playground

blocksauthtableformdisplayfeedbackailayoutnavigationoverlayactiondatastablebetaexperimental
Sankey diagram: 15 nodes, 30 links — blocks → form: 17; blocks → display: 16; blocks → ai: 10; blocks → layout: 10; blocks → navigation: 9; blocks → action: 8; blocks → overlay: 8; blocks → feedback: 7; auth → form: 8; auth → feedback: 4; auth → display: 1; auth → data: 1; table → data: 1; form → stable: 19; form → beta: 6; display → beta: 9; display → stable: 8; ai → experimental: 10; layout → stable: 7; layout → experimental: 2; layout → beta: 1; action → stable: 7; action → beta: 1; navigation → stable: 7; navigation → beta: 2; overlay → stable: 4; overlay → beta: 4; feedback → stable: 10; feedback → beta: 1; data → stable: 2
SourceTargetValue
blocksform17
blocksdisplay16
blocksai10
blockslayout10
blocksnavigation9
blocksaction8
blocksoverlay8
blocksfeedback7
authform8
authfeedback4
authdisplay1
authdata1
tabledata1
formstable19
formbeta6
displaybeta9
displaystable8
aiexperimental10
layoutstable7
layoutexperimental2
layoutbeta1
actionstable7
actionbeta1
navigationstable7
navigationbeta2
overlaystable4
overlaybeta4
feedbackstable10
feedbackbeta1
datastable2
<script lang="ts">
  import { Sankey } from '@urbicon-ui/blocks';

  const nodes = [
    { id: 'blocks', label: 'blocks', intent: 'primary' },
    { id: 'auth', label: 'auth', intent: 'secondary' },
    { id: 'table', label: 'table', intent: 'neutral' },
    { id: 'form', label: 'form' },
    { id: 'display', label: 'display' },
    { id: 'feedback', label: 'feedback' },
    { id: 'ai', label: 'ai' },
    { id: 'layout', label: 'layout' },
    { id: 'navigation', label: 'navigation' },
    { id: 'overlay', label: 'overlay' },
    { id: 'action', label: 'action' },
    { id: 'data', label: 'data' },
    { id: 'stable', label: 'stable', intent: 'success' },
    { id: 'beta', label: 'beta', intent: 'warning' },
    { id: 'experimental', label: 'experimental', intent: 'neutral' }
  ];
  const links = [
    { source: 'blocks', target: 'form', value: 17 },
    { source: 'blocks', target: 'display', value: 16 },
    { source: 'blocks', target: 'ai', value: 10 },
    { source: 'blocks', target: 'layout', value: 10 },
    { source: 'blocks', target: 'navigation', value: 9 },
    { source: 'blocks', target: 'action', value: 8 },
    { source: 'blocks', target: 'overlay', value: 8 },
    { source: 'blocks', target: 'feedback', value: 7 },
    { source: 'auth', target: 'form', value: 8 },
    { source: 'auth', target: 'feedback', value: 4 },
    { source: 'auth', target: 'display', value: 1 },
    { source: 'auth', target: 'data', value: 1 },
    { source: 'table', target: 'data', value: 1 },
    { source: 'form', target: 'stable', value: 19 },
    { source: 'form', target: 'beta', value: 6 },
    { source: 'display', target: 'beta', value: 9 },
    { source: 'display', target: 'stable', value: 8 },
    { source: 'ai', target: 'experimental', value: 10 },
    { source: 'layout', target: 'stable', value: 7 },
    { source: 'layout', target: 'experimental', value: 2 },
    { source: 'layout', target: 'beta', value: 1 },
    { source: 'action', target: 'stable', value: 7 },
    { source: 'action', target: 'beta', value: 1 },
    { source: 'navigation', target: 'stable', value: 7 },
    { source: 'navigation', target: 'beta', value: 2 },
    { source: 'overlay', target: 'stable', value: 4 },
    { source: 'overlay', target: 'beta', value: 4 },
    { source: 'feedback', target: 'stable', value: 10 },
    { source: 'feedback', target: 'beta', value: 1 },
    { source: 'data', target: 'stable', value: 2 }
  ];
</script>

<Sankey
  {nodes}
  {links}
  height={440}
  nodePadding={12}
/>

01 Examples

A SankeyNode has an id, a label, and an intent for its colour (primary, success, warning, danger, neutral, …). A SankeyLink joins a source id to a target id with a value. The layout sizes and places every node and band from those values, so you pass data, not coordinates.

Heating cost flow

A multi-source, multi-sink flow: each band's thickness is its share of the pot.
Gas billHeat pump electricityMaintenanceChimney sweepHeating cost potHeatingHot waterUnit 4Unit 4A
Sankey diagram: 9 nodes, 10 links — Gas bill → Heating cost pot: 2.206,09 €; Heat pump electricity → Heating cost pot: 1.127,31 €; Maintenance → Heating cost pot: 361,02 €; Chimney sweep → Heating cost pot: 128,00 €; Heating cost pot → Heating: 3.105,03 €; Heating cost pot → Hot water: 717,39 €; Heating → Unit 4: 1.855,47 €; Heating → Unit 4A: 1.249,56 €; Hot water → Unit 4: 394,77 €; Hot water → Unit 4A: 322,62 €
SourceTargetValue
Gas billHeating cost pot2.206,09 €
Heat pump electricityHeating cost pot1.127,31 €
MaintenanceHeating cost pot361,02 €
Chimney sweepHeating cost pot128,00 €
Heating cost potHeating3.105,03 €
Heating cost potHot water717,39 €
HeatingUnit 41.855,47 €
HeatingUnit 4A1.249,56 €
Hot waterUnit 4394,77 €
Hot waterUnit 4A322,62 €
<div class="w-full max-w-4xl">
  <Sankey nodes={heatingNodes} links={heatingLinks} formatValue={formatEur} height={500} />
</div>

Salary breakdown

A node can be both a target and a source: social contributions receive from gross pay, then split again.
Gross salaryIncome taxSocial contributionsHealth insurancePension insuranceUnemployment ins.Long-term careNet pay
Sankey diagram: 8 nodes, 7 links — Gross salary → Income tax: 850 €; Gross salary → Social contributions: 1.080 €; Social contributions → Health insurance: 360 €; Social contributions → Pension insurance: 480 €; Social contributions → Unemployment ins.: 90 €; Social contributions → Long-term care: 150 €; Gross salary → Net pay: 2.570 €
SourceTargetValue
Gross salaryIncome tax850 €
Gross salarySocial contributions1.080 €
Social contributionsHealth insurance360 €
Social contributionsPension insurance480 €
Social contributionsUnemployment ins.90 €
Social contributionsLong-term care150 €
Gross salaryNet pay2.570 €
<div class="w-full max-w-3xl">
  <Sankey
    nodes={salaryNodes}
    links={salaryLinks}
    formatValue={(v) => `${v.toLocaleString('de-DE')}`}
    height={400}
  />
</div>

Node alignments

Four layer alignments. A node whose links end before the last layer (B here) lands differently: justify pushes it to the right edge, left keeps it at its source layer.

justify

SourceMidAB (early sink)
Sankey diagram: 4 nodes, 3 links — Source → Mid: 60; Mid → A: 60; Source → B (early sink): 30
SourceTargetValue
SourceMid60
MidA60
SourceB (early sink)30

left

SourceMidAB (early sink)
Sankey diagram: 4 nodes, 3 links — Source → Mid: 60; Mid → A: 60; Source → B (early sink): 30
SourceTargetValue
SourceMid60
MidA60
SourceB (early sink)30

right

SourceMidAB (early sink)
Sankey diagram: 4 nodes, 3 links — Source → Mid: 60; Mid → A: 60; Source → B (early sink): 30
SourceTargetValue
SourceMid60
MidA60
SourceB (early sink)30

center

SourceMidAB (early sink)
Sankey diagram: 4 nodes, 3 links — Source → Mid: 60; Mid → A: 60; Source → B (early sink): 30
SourceTargetValue
SourceMid60
MidA60
SourceB (early sink)30
{#each ['justify', 'left', 'right', 'center'] as align (align)}
  <div class="border-border-subtle bg-surface-elevated rounded-contain border p-3">
    <h4 class="text-text-secondary mb-2 text-xs font-medium tracking-wide uppercase">
      {align}
    </h4>
    <Sankey
      nodes={alignmentNodes}
      links={alignmentLinks}
      formatValue={(v) => `${v}`}
      height={180}
      nodeAlign={align as 'justify' | 'left' | 'right' | 'center'}
    />
  </div>
{/each}

Compact embedding

A small height inside a card; the layout adapts to fit.

Cashflow Overview

Q1 2026

SourceTarget
Sankey diagram: 2 nodes, 1 links — Source → Target: 100 %
SourceTargetValue
SourceTarget100 %
<div class="border-border-subtle bg-surface-elevated w-full max-w-md rounded-2xl border p-5">
  <header class="mb-3">
    <h3 class="text-text-primary text-sm font-semibold">Cashflow Overview</h3>
    <p class="text-text-tertiary text-xs">Q1 2026</p>
  </header>
  <Sankey
    nodes={minimalNodes}
    links={minimalLinks}
    formatValue={(v) => `${v} %`}
    height={140}
    nodeWidth={16}
    nodePadding={10}
  />
</div>

02 Customization

Custom tooltip

The tooltip snippet receives the hovered datum and a kind of 'node' or 'link'.
Gas billHeat pump electricityMaintenanceChimney sweepHeating cost potHeatingHot waterUnit 4Unit 4A
Sankey diagram: 9 nodes, 10 links — Gas bill → Heating cost pot: 2.206,09 €; Heat pump electricity → Heating cost pot: 1.127,31 €; Maintenance → Heating cost pot: 361,02 €; Chimney sweep → Heating cost pot: 128,00 €; Heating cost pot → Heating: 3.105,03 €; Heating cost pot → Hot water: 717,39 €; Heating → Unit 4: 1.855,47 €; Heating → Unit 4A: 1.249,56 €; Hot water → Unit 4: 394,77 €; Hot water → Unit 4A: 322,62 €
SourceTargetValue
Gas billHeating cost pot2.206,09 €
Heat pump electricityHeating cost pot1.127,31 €
MaintenanceHeating cost pot361,02 €
Chimney sweepHeating cost pot128,00 €
Heating cost potHeating3.105,03 €
Heating cost potHot water717,39 €
HeatingUnit 41.855,47 €
HeatingUnit 4A1.249,56 €
Hot waterUnit 4394,77 €
Hot waterUnit 4A322,62 €
<div class="w-full max-w-3xl">
  <Sankey nodes={heatingNodes} links={heatingLinks} formatValue={formatEur} height={400}>
    {#snippet tooltip(datum, kind)}
      {#if kind === 'node'}
        {@const node = datum as { id: string; value: number }}
        <span class="block font-medium">{node.id}</span>
        <span class="text-text-tertiary text-2xs block tabular-nums">
          {formatEur(node.value)}
        </span>
      {:else}
        {@const link = datum as {
          source: { id: string };
          target: { id: string };
          value: number;
        }}
        <span class="block font-medium">
          {link.source.id}{link.target.id}
        </span>
        <span class="text-primary text-2xs block font-semibold tabular-nums">
          {formatEur(link.value)}
        </span>
      {/if}
    {/snippet}
  </Sankey>
</div>

03 Accessibility

SVG with role="img"

The SVG has role="img" with an aria-label that lists every connection ("Source → Target: value"). A screen reader announces the diagram in a single block.

Table fallback

An sr-only table (source / target / value) is rendered in addition — screen readers that prefer tables can query the data row by row.

Keyboard navigation

Tab focuses every node and link in sequence. On a focused element, Enter/Space triggers the click handler (onNodeClick / onLinkClick).

Hover and focus highlight

Hovering or focusing a node dims all unconnected paths and nodes and highlights its direct connections.

04 API Reference

27 props
27 props 2 required
Prop
Type
Default
Description

05 Types

Local type definitions used by this component.

10 types
Name
Kind
Category
Used by
Description

06 Installation

Import

import { Sankey } from '@urbicon-ui/blocks';