Skip to main content

WaterfallChart

Comparisonchart

Gross-to-net bridge: running-total steps and subtotal bars showing what added and subtracted along the way.

import { WaterfallChart } from "@elabs-ai/components-charts";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui WaterfallChart chart (from @elabs-ai/components-charts) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look WaterfallChart up before writing code: `npx -y @elabs-ai/components-cli docs WaterfallChart` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-charts is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Use it for

  • How do these compare? A bridge between totals.
  • a running total with signed steps into and out of it

Avoid

  • there is no meaningful running total — use diverging bars instead
  • A `total` row whose value does not actually equal the running total at that point — the connector into it visibly plunges/jumps instead of landing flat, which is the bridge silently telling you the reconciliation is wrong.
  • Reading the dashed connector as if it carries a value — it is furniture that hands off between steps, never a series; the story is the bar heights, not the connector's slope.
  • Entering a reduction as a positive `value` (or otherwise fighting the sign convention) — color and the +/− label are derived from the actual before/after delta, not from caller intent, so a flipped sign silently mislabels the step.
  • More than ~6-8 steps in one bridge — past that the running total stops being followable and the connectors turn into visual noise; split into a top-level bridge plus a drill-down instead.

Works with

Contains

Examples

16 live examples

Default

Gross → refunds → COGS → ops → net: connectors hand off at the running total, totals draw from zero, labels are signed.

Loading

`status="loading"` (RM-182): a skeleton in the plot box the chart will fill, with one polite status message per chart, at 380, 600 and 900 px.

Showcase

Everything the parity waves added to `WaterfallChart`, composed on one realistic bridge: monthly steps grouped into an auto-inserted quarterly subtotal (`subtotalBy="quarter"`), decreases sorted first within each quarter (`sort="decreasesFirst"`), every row labelled with its percent change in its own row colour (`labels={{ totals: "all", differences: "percent", matchColor: true }}`), thicker hand-off connectors (`connectors="thick"`), and a row note, a target line and a floating note (`annotations`). `ChartFrame` adds the title, description, notes, source and export chrome around it. Left out: `zoomToDifferences` and `dataFormat="runningTotals"` — the first tells a different story (a huge, near-flat balance whose swings are tiny beside it, not this bridge's additive quarterly growth) and the second is an input-format convenience with no visual difference from the delta rows used here.

Horizontal

Same data, horizontal orientation — the label-fitting `BarYAxis` plan.

Unit Rungs

`unit` renders each bar as a counted `UnitStack` of rungs — the F9 look — instead of a solid capsule. The pitch comes from the value scale, so one rung is worth the same amount in every step (#241) — stated here since the waterfall renders no axis of its own to read it from.

With Callouts

`callouts` names the one or two steps that actually explain the bridge — drawn above the step with a `Leader`, so a reader does not have to infer "which of these five bars is the story" from height alone. Needs extra `margin.top` for the note's own headroom.

No Connectors No Labels

Connectors and value labels can both be turned off for a quieter read.

Drilldown

Click a step to drill into it; the detail panel below reports the datum.

Keyboard Drilldown

The same drill-down reached with the keyboard only — one tab stop for the whole chart, ArrowRight to traverse every step, Enter to activate.

Running Totals

`dataFormat="runningTotals"` reads every row as the running total reached at that point instead of a signed delta — the same bridge as `Default`, converted once on the way in.

Quarterly Subtotals

`subtotalBy` auto-inserts a subtotal checkpoint after each run of rows sharing that row field's value — here, one after every quarter's months.

Sorted Decreases First

`sort="decreasesFirst"` reorders the steps WITHIN each subtotal- or total-bounded group — the checkpoints themselves never move.

Zoomed To Differences

`zoomToDifferences` drops the zero baseline when a checkpoint sits far above the steps' own swing — a `"total"`/`"subtotal"` row can no longer honestly draw as a zero-based bar once zero is off-screen, so it renders as a point on a dashed stem instead; the steps themselves stay ordinary bars, since a delta's length is proportional under any linear domain.

Percent Difference Labels

`labels` with `differences: "percent"` reads each step as a signed percent of the running total it left off, instead of its absolute value — `matchColor` paints each label in its own row's ink (through `seriesLabelInk`, never the raw fill, #544) instead of the neutral halo ink; the mixed increase/decrease/subtotal rows exercise the `layoutLabels` collision pass (RM-122) against both the annotation and axis obstacles.

Thick Connectors

`connectors="thick"` weights the hand-off hairlines for a bolder read.

Selection states

A host's `selectionStates` paints Refunds selected, COGS associated and Ops excluded: the inner BarChart (not Waterfall itself) resolves and paints the tri-state, so the effect is visible on the step bars without Waterfall knowing anything about the paint rules itself.

API

WaterfallChart

PropTypeDescription
messagesChartMessagesmessages group (RM-187): this chart's own words, keyed by the ui catalogue's `charts.*` message keys. A key set here wins over the `LocaleProvider`; every other key reads the catalogue as before.
datarequiredWaterfallDatum[]Steps from gross to net — one row per bar.
orientationBarOrientationDefault `"vertical"`.
showValuesboolean@deprecated Use `labels` — `true`/`false` keep meaning the same thing (ADR 0042 A.3, row 16). Read until 7.0.0, with one development warning; when both are set, `labels` wins.
connectorsboolean | "thin" | "thick"Dashed hand-off hairline between each step's end and the next step's start: `false` draws none, `true`/`"thin"` the default weight, `"thick"` a heavier one (RM-122). Default `true`.
dataFormatWaterfallDataFormat`"differences"` (default): `data` values are signed deltas. `"runningTotals"`: every row's `value` is the running total AT that row — converted once, up front (RM-122 `waterfall-steps.ts`).
subtotalBystringAuto-inserts a `kind: "subtotal"` checkpoint after each run of rows sharing this field's value (RM-122) — e.g. `subtotalBy="quarter"` with `{ label, value, quarter: "Q1" }` rows. Off by default.
subtotalLabelstringThe inserted subtotal row's label — a `"{group}"` template. Default `"{group} subtotal"`. Ignored without `subtotalBy`.
sortWaterfallSortReorders `"step"` rows within each subtotal group (RM-122). Default `"data"` (spreadsheet order).
startWaterfallEndpointOptionsShow/relabel the chart's own first row (RM-122, "start column").
endWaterfallEndpointOptionsShow/relabel the chart's own last row (RM-122, "end column").
zoomToDifferencesbooleanDrops the zero baseline when a checkpoint sits far above the steps' own swing, drawing `"total"`/`"subtotal"` rows as points instead of bars (RM-122; see `computeWaterfallZoomDomain`). Default `false`.
labelsboolean | WaterfallLabelsConfigSigned value label on each step (`HaloText`, 800 weight; `true`/`false`), or per-row control (RM-122) — see {@link WaterfallLabelsConfig}. Default `true`.
gridbooleanThe value-axis gridlines. Turn off when every bar already carries its own value label (`labels`) and an unlabelled gridline would only add furniture with no tick to read it against. Default `true`.
positiveFillstringFill for an increasing step. Default `var(--chart-1)`.
negativeFillstringFill for a decreasing step. Default `var(--chart-2)`.
totalFillstringFill for a `"total"` row. Default `var(--chart-foreground)`.
unitnumberWhen set, render each bar as a counted `UnitStack` of rungs (the F9 look) instead of a solid capsule — one rung per `unit` of value. Off by default.
valueFormatChartValueFormatValue/label format. Default `"compact"`.
calloutsWaterfallCallout[]The one or two steps that actually explain the bridge, named directly on the chart (see {@link WaterfallCallout}). Default: none.
plotHeightResponsive<ChartPlotHeight>The plot's own height (ADR 0039): px, or `{ aspect }` (width ÷ height), optionally per breakpoint. Default: 2 : 1, and 1.25 : 1 when narrow.
heightnumber@deprecated Use `plotHeight` (`height={n}` is an alias for `plotHeight={n}`); removed in 7.0.0.
marginnumber | Partial<Margin>Chart margins: one number for every side, or per side. Default: 40 on every side.
classNamestringAdditional class name for the container.
statusChartStatusLoading vs ready (RM-182). `"loading"` shows a skeleton in the plot box the chart will fill, with one polite status message, until the data is ready. Default: `"ready"`.
accessibleLabelChartA11yProps["accessibleLabel"]Accessible name for the chart region.
accessibleDescriptionChartA11yProps["accessibleDescription"]Supplemental description read by AT.
annotationsreadonly ChartAnnotation[]Declarative annotations in data units: text notes, ranges, reference lines, row notes (a step's `label` is its category). Painted by the inner `BarChart`.
analyticsreadonly ChartAnalytic[]Computed lines and bands (RM-138, ADR 0040 §1), painted by the inner `BarChart`. `of` defaults to the drawn running total (`"__cumulative"`); `of: "value"` reduces the step values instead.
paletteChartPaletteColour ramp for the steps (RM-186): increases take the palette's gain colour and decreases its loss colour (`"diverging"` is the sign pair, any other palette its first two colours). `positiveFill` / `negativeFill` still win. Unset: `--chart-1` / `--chart-2`, as before.
onDatapointClickChartDatapointClickHandler<WaterfallStep>Fires when a datapoint is activated by pointer OR keyboard. Setting it is what enables the interaction layer — with it unset the chart renders exactly as before, with no extra DOM and no new focusables.
copyValueOnActivatebooleanPut the datapoint's EXACT value on the clipboard when it is activated — the recovery path for compact axis labels and tooltips, reachable by mouse AND keyboard because activation rides `ChartDatapointLayer`'s real buttons. Default `false` on a raw chart container: turning it on mounts the interaction layer, and the documented guarantee is that a chart with no interaction props renders byte-identical DOM. `AutoChart` — the spec-driven surface, where the caller never sees the axis config — turns it on by default. A consumer-supplied `onDatapointClick` always wins; the two never both run.
datapointLabelChartDatapointLabel<WaterfallStep>Override the accessible name of each keyboard target.
maxInteractiveDatapointsnumberDev-warning threshold on the number of keyboard targets. Default 500. NOT a functional cap — every plotted point stays reachable, so a keyboard user can reach exactly what a mouse user can click.
selectionStatesChartSelectionStatesResolver<WaterfallRow>Maps a mark (category, optional series key, optional raw row) to its selection state. Unset → the chart renders exactly as before.
dimExcludedbooleanDim `excluded` marks and add their non-hue channel (the dashed frame). Default `true`; `false` keeps the `data-selection` attribute (so a host can style it) but paints nothing.

Also accepts: ChartInteractionProps<WaterfallStep>, ChartSelectionProps<WaterfallRow>, FrameSizeGroupProps, Pick<ChartStateGroupProps, "status">, Pick<ValueFormatGroupProps, "valueFormat">

Grid

PropTypeDescription
modeGridModeGrid painting mode (RM-108). Default: `"lines"`.
horizontalbooleanShow horizontal grid lines. Default: true
verticalbooleanShow vertical grid lines. Default: false
numTicksRowsnumberNumber of horizontal grid lines. Default: the height-derived target shared with `YAxis` (`tickTargetForHeight(innerHeight)` — 3 under 200 px, else 5), so rows and labels stay aligned (RM-108).
numTicksColumnsnumberNumber of vertical grid lines. Default: 10
rowTickValuesnumber[]Explicit tick values for horizontal grid lines. Overrides numTicksRows.
strokestringGrid line stroke color. Default: var(--chart-grid)
loadingStrokestringGrid stroke while loading chrome is active. Falls back to `stroke`.
strokeOpacitynumberGrid line stroke opacity. Default: 1
strokeWidthnumberGrid line stroke width. Default: `CHART_HAIRLINE_WIDTH` — the one furniture weight.
strokeDasharraystringGrid line dash array. Default: "4,4" for dashed lines
highlightRowValuesnumber[]Horizontal row values rendered with alternate styling (e.g. zero baseline).
highlightRowStrokestringStroke for highlighted rows. Default: var(--chart-foreground-muted)
highlightRowStrokeOpacitynumberStroke opacity for highlighted rows. Default: 1
highlightRowStrokeWidthnumberStroke width for highlighted rows. Default: 1
highlightRowStrokeDasharraystringDash array for highlighted rows. Default: solid line
highlightRowLabel(value: number) => string | undefinedText label drawn at the right edge of each `highlightRowValues` entry (`HaloText`, so it survives sitting on the grid) — a KPI target/goal line the reader can name, not just see. Return `undefined`/`""` to skip the label for a given value. Unset (default): no label, byte-identical to before this prop existed.
highlightColumnValuesArray<number | Date>Vertical reference lines at these `xDataKey`-domain values (e.g. "today" on a forecast chart) — the column counterpart of `highlightRowValues`. Default: none.
highlightColumnStrokestringStroke for highlighted columns. Default: var(--chart-foreground-muted)
highlightColumnStrokeOpacitynumberStroke opacity for highlighted columns. Default: 1
highlightColumnStrokeWidthnumberStroke width for highlighted columns. Default: 1
highlightColumnStrokeDasharraystringDash array for highlighted columns. Default: solid line
highlightColumnLabel(value: number | Date) => string | undefinedText label drawn at the bottom-inside, left of each `highlightColumnValues` entry (`HaloText`), e.g. "Today". Return `undefined`/`""` to skip the label for a given value. Unset (default): no label.
fadeHorizontalbooleanEnable horizontal fade effect on grid rows (fades at left/right). Default: true
fadeVerticalbooleanEnable vertical fade effect on grid columns (fades at top/bottom). Default: false
yAxisIdstring | numberY-scale for horizontal grid lines. Default: primary (`"left"`) axis.
shimmerbooleanAnimate a shimmer band across horizontal grid lines. Default: false
shimmerStrokestringShimmer band stroke (color and opacity via color-mix or oklch alpha).
shimmerLengthnumberShimmer band width in pixels. Default: 140
shimmerSpeednumberShimmer speed multiplier (higher = faster). Default: 1
shimmerSyncbooleanMatch loop timing to the loading line pulse (cycle + inter-loop pause).

BarXAxis

PropTypeDescription
tickerHalfWidthnumberWidth of the date ticker box for fade calculation. Default: 50
showAllLabelsbooleanKeep every category (never stride over labels). Default: false. NOTE this no longer forces HORIZONTAL rendering: with dropping disabled the axis may still tilt, ellipsize, or hide itself when the labels cannot fit.
maxLabelsnumberMaximum number of labels to show. Default: 12
fitCategoryAxisFitHow labels that do not fit their band are rescued (RM-108): - `"auto"` (default) — horizontal → two-line `wrap` → 45° tilt → ellipsis → stride → hidden; - `"wrap"` — the same cascade without the tilt rung (never rotates); - `"tilt"` — the pre-RM-108 cascade (skips `wrap`); - `"off"` — pins the pre-fit behaviour: full labels, count-capped stride, no measurement and no reserved axis space. The regression escape hatch.
titleReactNodeAxis title — names what the categories are (RM-188; the same part `XAxis` draws).
titlePlacementAxisTitlePlacement`"outside"` (default): the title sits under the labels, flush with the plot's end edge. `"inside"`: hung inside the plot's bottom-end corner — the same fixed corner `XAxis` uses. By design it does not step around marks, so on a vertical bar chart it paints over the LAST bar (its halo keeps it legible); use `"outside"` when that bar must stay clear.

BarYAxis

PropTypeDescription
showAllLabelsbooleanKeep every category (never stride over labels). Default: true. NOTE this no longer implies every label renders in full: the axis still ellipsizes to the gutter it has, and hides itself when nothing legible fits.
maxLabelsnumberMaximum number of labels to show. Default: 20
fitCategoryAxisFit`"off"` pins the pre-fit behaviour — full labels, count-capped stride, no measurement and no reserved gutter. The regression escape hatch. Default: `"auto"`.
maxWidthnumberThe widest the label gutter may grow, in px. The gutter is measured from the labels and capped so a long name cannot eat the plot; the default cap suits one or two words. Raise it for rows whose names ARE the content — "Send Invoice → Payment Reminder" — and the chart still keeps its own minimum plot width. Default: 112.
titleReactNodeAxis title — names what the rows are (RM-188; the same part `YAxis` draws).
titlePlacementAxisTitlePlacement`"outside"` (default): the title sits above the top row label, in the label gutter. `"inside"`: hung inside the plot's top-start corner — the same fixed corner `YAxis` uses. By design it does not step around marks, so on a horizontal bar chart it paints over the FIRST bar (its halo keeps it legible); use `"outside"` when that bar must stay clear.

ChartTooltip

PropTypeDescription
showDatePillbooleanWhether to show the date pill under the plot. Default: shown when it fits the chart's bottom margin (≥ 36px) — never painted over a small plot.
showCrosshairbooleanWhether to show the vertical crosshair line. Default: true
showDotsbooleanWhether to show dots on the lines. Default: true
indicatorColorstring | ((point: Record<string, unknown>) => string)Color for the crosshair/indicator line. When a function, receives the hovered point (e.g. for candlestick: match candle color from close vs open). Default: --chart-crosshair.
content(props: { point: Record<string, unknown>; index: number }) => React.ReactNodeCustom content renderer for the tooltip box
rows(point: Record<string, unknown>) => TooltipRow[]Custom row renderer - return array of TooltipRow
variantChartTooltipVariantBox layout preset (RM-119). `"rows"` (default) is today's stacked `ChartTooltipContent`. `"table"` renders `ChartTooltipTable` — one column per series with a header row, the date in the `<caption>` (Datawrapper's dual-axis / multi-series shape, `dw-charts.md` §2.22). `"inline"` renders no box at all — `ChartTooltipInline` paints the hovered (or `focus`-nearest) series' value directly at the mark with `HaloText` (the bikes chart). Ignored when `content` is set — a custom renderer already owns the whole box.
focusbooleanDrive RM-112's per-series dim (`SeriesHoverDim`) from the tooltip's OWN nearest-series resolution: whichever series' y position is closest to the pointer, at ANY x along the crosshair, counts as hovered — not only a direct hover on that series' own 2px stroke. Works standalone: it registers "focus requested" on the shared series-mode context, so the dim fires with no `focusOnHover` prop on the `LineChart`/`AreaChart` container (a container `focusOnHover` still works exactly as before, and the two compose — either one turns the dim on). Also the default target series for `variant="inline"`. Default false.
pinbooleanTap-to-pin (RM-119): on a coarse pointer, a tap keeps the tooltip open instead of clearing the instant the finger lifts. A second tap on the mark, `Esc`, or a tap outside the chart releases it (announced once via `role="status" aria-live="polite"`). Default: `true` only when the pointer is coarse (`(pointer: coarse)` media query); pass an explicit boolean to force either way regardless of pointer type.
valueInTitlebooleanSwap the tooltip box's own title for nothing — the hovered value moves to the chart's OWN title instead (RM-119, #610). Inside a `ChartFrame` that shows a title this is automatic: while a row is hovered, pinned or keyboard-focused the frame's title reads "Apr: Revenue 40" and is announced once through the frame's polite status; leaving restores the title. No wiring. Outside a frame (or in `chrome="bare"`, a custom `headerSlot`, the expanded dialog) only the box title is dropped — the date/category is still on `useChart().tooltipData` for a custom title. Default false.
dotColorstring | ((point: Record<string, unknown>, line: LineConfig) => string)Override tooltip dot fill. When omitted and `rows` is set, dot colors match row colors. When a function, receives the hovered point and line config.
childrenReact.ReactNodeAdditional content to show below rows (e.g., markers)
classNamestringCustom class name
springConfigSpringConfigPer-chart override for the crosshair / dot / date-pill spring.
boxSpringConfigSpringConfigPer-chart override for the floating-panel spring.
panelStyleReact.CSSPropertiesInline styles for the tooltip panel (background, blur, etc.).
unitstringUnit text appended to every DEFAULT row's value (RM-109) — the same `unit` `<YAxis>` paints on one tick. Set automatically from the chart's own `<YAxis unit>` when unset (the shell threads it through); pass it explicitly only to override. Ignored when `rows` is set — a custom row builder owns its own `TooltipRow.unit` per row.
valueFormatChartValueFormatSTYLE to borrow from the chart's own `<YAxis valueFormat>` for DEFAULT rows (RM-109) — a currency symbol, a percent sign — never abbreviation (`ChartTooltipContent`'s doc). Set automatically from `<YAxis valueFormat>` when unset; ignored when `rows` is set.
currencystringISO 4217 code for `valueFormat`'s `style: "currency"`.

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
connectorvar(--chart-foreground-muted), 0.6px, dashed (furniture, not data — Leader's default)
decreasevar(--chart-2) (default negativeFill)
increasevar(--chart-1) (default positiveFill)
totalvar(--chart-foreground) (default totalFill)
valueLabelvar(--chart-foreground) on a var(--chart-background) halo (HaloText)