Skip to content
ShineUI
Install

Waterfall chart

Bridges from one total to the next.

<sui-chart-waterfall>ChartsStable

About

Waterfall, or bridge, chart: the accounting walk from one total to the next, revenue down to net income. Increases take the second ramp step, decreases the accent, totals the first ramp step, with dashed connectors carrying the running level between steps. Every bar prints its value; hovering a step dims the others and shows the step and the running level.

Usage

<sui-chart-waterfall heading="Net income bridge" subtitle="Jan 1 to Jul 31, 2026 · the accounting walk from revenue to net" caption="Dashed connectors carry the running level from one step to the next" money></sui-chart-waterfall> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-waterfall"); el.items = [ { label: "Revenue", value: 1119520, total: true }, { label: "COGS", value: -587915 }, { label: "Payroll", value: -289400 }, { label: "Rent", value: -42000 }, { label: "Other opex", value: -105230 }, { label: "Interest", value: -1890 }, { label: "Net income", value: 93085, total: true }, ]; /* SuiWaterfallItem[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartWaterfall } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Revenue", value: 1119520, total: true }, { label: "COGS", value: -587915 }, { label: "Payroll", value: -289400 }, { label: "Rent", value: -42000 }, { label: "Other opex", value: -105230 }, { label: "Interest", value: -1890 }, { label: "Net income", value: 93085, total: true }, ]; /* SuiWaterfallItem[] */ return <SuiChartWaterfall heading="Net income bridge" subtitle="Jan 1 to Jul 31, 2026 · the accounting walk from revenue to net" caption="Dashed connectors carry the running level from one step to the next" money items={items} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartWaterfall } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Revenue", value: 1119520, total: true }, { label: "COGS", value: -587915 }, { label: "Payroll", value: -289400 }, { label: "Rent", value: -42000 }, { label: "Other opex", value: -105230 }, { label: "Interest", value: -1890 }, { label: "Net income", value: 93085, total: true }, ]; // SuiWaterfallItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartWaterfall heading="Net income bridge" subtitle="Jan 1 to Jul 31, 2026 · the accounting walk from revenue to net" caption="Dashed connectors carry the running level from one step to the next" money :items="items" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartWaterfall } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartWaterfall], template: `<sui-chart-waterfall heading="Net income bridge" subtitle="Jan 1 to Jul 31, 2026 · the accounting walk from revenue to net" caption="Dashed connectors carry the running level from one step to the next" money [items]="items" (suiHover)="onChange($event)"></sui-chart-waterfall>`, }) export class Example { items = [ { label: "Revenue", value: 1119520, total: true }, { label: "COGS", value: -587915 }, { label: "Payroll", value: -289400 }, { label: "Rent", value: -42000 }, { label: "Other opex", value: -105230 }, { label: "Interest", value: -1890 }, { label: "Net income", value: 93085, total: true }, ]; // SuiWaterfallItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Increases take --viz-2, decreases --accent and totals --viz-1; a total after the first restates the running level rather than its own value. The plot is one tab stop named by an aria-label on the host, else by the heading and every step, the arrow keys reach each one, and the color key names the three.

<sui-chart-waterfall heading="Net income bridge" subtitle="Jan 1 to Jul 31, 2026 · the accounting walk from revenue to net" caption="Dashed connectors carry the running level from one step to the next" money></sui-chart-waterfall>
// setup(root): properties and events setup(root) { root.querySelector('sui-chart-waterfall').items = netIncome(); }

Counts

Without money the axis, the bar labels and the tooltip print plain counts, which is the default; the bridge above sets the attribute. currency names the ISO code, USD by default, and the page's locale places its symbol: currency="EUR" on a German page reads 120.000 €.

<sui-chart-waterfall heading="Open invoice count" subtitle="Movement in the open ledger · July"></sui-chart-waterfall>
// setup(root): properties and events setup(root) { root.querySelector('sui-chart-waterfall').items = [ { label: 'Opening', value: 412, total: true }, { label: 'Issued', value: 268 }, { label: 'Paid', value: -301 }, { label: 'Voided', value: -14 }, { label: 'Closing', value: 365, total: true }, ]; }

Events

suiHover fires whenever the pointer changes step and reports { index }, with index -1 on leave. hoverIndex is mutable: the buttons write it directly, which moves the highlight and the tooltip but emits nothing, so the log holds what the pointer last reported.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-waterfall id="wf-events" heading="Net income bridge" subtitle="Hover a step, or use the buttons" money></sui-chart-waterfall> <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center"> <sui-button id="wf-cogs" size="sm" variant="outline" label="Show COGS"></sui-button> <sui-button id="wf-net" size="sm" variant="outline" label="Show net income"></sui-button> <sui-button id="wf-clear" size="sm" variant="ghost" label="Clear"></sui-button> <span id="wf-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#wf-events'); const log = root.querySelector('#wf-log'); const items = netIncome(); el.items = items; el.addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `Step ${i + 1} of ${items.length}: ${items[i].label}.`; }); root.querySelector('#wf-cogs').addEventListener('click', () => { el.hoverIndex = 1; }); root.querySelector('#wf-net').addEventListener('click', () => { el.hoverIndex = 6; }); root.querySelector('#wf-clear').addEventListener('click', () => { el.hoverIndex = -1; }); }

Empty and loading

loading stands a shimmer in for the plot and marks the host aria-busy while the figures are on their way; the frame and its heading stay. A chart with nothing to plot keeps its frame and says so, and emptyMessage is the sentence. Both are the same on every chart.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-waterfall heading="Net income bridge" subtitle="Fetching the period" height="150" loading></sui-chart-waterfall> <sui-chart-waterfall heading="Net income bridge" subtitle="This period" height="150" empty-message="No movement was posted this period."></sui-chart-waterfall> </div>

Keyboard

KeyAction
TabMoves to the plot. The color key under it does not toggle, so it takes no stop.
ArrowRight ArrowLeftMoves to the next or the previous step and shows its tooltip; a reader hears the step and the running total after it.
Home EndMoves to the first or the last step.
EscapeLeaves the point and hides the tooltip; the plot keeps focus.

API

<sui-chart-waterfall>

Properties

PropertyAttributeTypeDefaultDescription
captioncaptionstringstring | undefinedFootnote under the legend.
currencycurrencystring'USD'The ISO 4217 code money prints in: USD by default, EUR, JPY. The page's locale (setLocale(), else <html lang>) places the symbol and sets the grouping and the decimal mark, so EUR on a German page reads 1,2 Mio. € on the axis and 121.695 € in the tooltip. An unknown code prints the bare figure. Ignored without money.
emptyMessageempty-messagestringstring | undefinedSentence drawn over the plot when there are no steps or every step is zero. Defaults to "Nothing to plot yet."; the frame, the axis and the legend stay. The chart filters nothing, so this is its only empty string.
everyeverySuiChartEvery"auto" | number'auto'Draw every nth step label, so a dense axis stays legible. auto, the default, measures the widest label against the space one step is given and thins only where the labels would collide; where they fit it draws all of them and the axis is what it always was. Set it to 1 to draw every label at any width, or to a larger number for the stride to start from: where its labels would run into each other it widens to the next multiple that clears them.
fixedSizefixed-sizebooleanfalsePin the plot to width, which is what a fixed export wants, or a row of charts that have to measure the same whatever the space around them. Without it the plot grows to the width the container gives it and the coordinate space tracks the pixel width, so one unit stays one pixel: the steps spread out and the axis text keeps the size it was drawn at. Reflected.
headingheadingstringstring | undefinedChart heading (the reference title prop, renamed because title is a native attribute). With what the chart shows, it names the plot for a reader; an aria-label or aria-labelledby on the host names the plot instead, and this summary then describes it.
heightheightnumber210ViewBox height of the plot in px.
hoverIndexhover-indexnumber-1Hovered step index; -1 when idle. The pointer and the arrow keys set it. A write from outside moves the highlight and the tooltip and emits nothing, so a bridge can sit in a sui-chart-synced group beside a line of the same periods.
itemsproperty onlySuiWaterfallItem[][]Bridge steps in order. A total item starts at zero: the first sets the running level from its value, and every later one restates the running level and ignores its own value, so a bridge that nets to zero draws its closing bar flat on the baseline. Increases paint --sui-chart-waterfall-increase, the second ramp step by default; decreases paint --sui-chart-waterfall-decrease, the accent by default; totals paint --sui-chart-waterfall-total, the first ramp step by default. The legend and the tooltip key follow. Set them on the element or any ancestor. Any CSS color or var(--token) works; use a token so the color follows the theme. Property, not attribute.
loadingloadingbooleanfalseThe figures are still on their way: a shimmer stands in for the plot, the frame and its heading stay, and the host is aria-busy. Reflected, so a page can style the wait.
moneymoneybooleanfalseMoney formatting in currency: compact on the axis and the step figures ($120k), exact in the tooltip ($121,695). Off, the figures print plainly, which is what a count wants; set it for a balance. Reflected.
subtitlesubtitlestringstring | undefinedOne-line context under the title.
widthwidthnumber460Width of the plot in px. The plot grows to the width its container gives it, and this is the starting figure, used until the container has been measured and wherever it measures zero; with fixedSize it is the plot's exact width. The card adds 34px of padding and border on top of it.

Events

EventDetailReactVueDescription
suiHoverSuiChartHoverDetailonSuiHover@sui-hoverFires when the pointer or the keyboard moves the hover to another step; { index: -1 } when the pointer leaves, or Escape or a blur takes the keyboard's point away.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the plot, the chart's one tab stop, from which the arrow keys reach the steps. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the title.

Types

SuiChartEvery typeHow the x labels thin: a fixed stride, or auto to measure the widest label against the room one point is given and draw only as many as fit.
No members.
SuiChartHoverDetail interfaceDetail of suiHover: the hovered index, or -1 when the pointer leaves the plot. What the index counts is each chart's own: an x position here, a point on a scatter, an axis on a radar.
indexnumber
SuiWaterfallItem interfaceOne step of the bridge. total bars start at zero (the first sets the running level, later ones restate it).
labelstring
valuenumber
total?boolean

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also