import { WaterfallChart } from "@elabs-ai/components-charts";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-chartsPaste 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
Used inside
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
| Prop | Type | Description |
|---|---|---|
messages | ChartMessages | messages 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. |
datarequired | WaterfallDatum[] | Steps from gross to net — one row per bar. |
orientation | BarOrientation | Default `"vertical"`. |
showValues | boolean | @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. |
connectors | boolean | "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`. |
dataFormat | WaterfallDataFormat | `"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`). |
subtotalBy | string | Auto-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. |
subtotalLabel | string | The inserted subtotal row's label — a `"{group}"` template. Default `"{group} subtotal"`. Ignored without `subtotalBy`. |
sort | WaterfallSort | Reorders `"step"` rows within each subtotal group (RM-122). Default `"data"` (spreadsheet order). |
start | WaterfallEndpointOptions | Show/relabel the chart's own first row (RM-122, "start column"). |
end | WaterfallEndpointOptions | Show/relabel the chart's own last row (RM-122, "end column"). |
zoomToDifferences | boolean | Drops 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`. |
labels | boolean | WaterfallLabelsConfig | Signed value label on each step (`HaloText`, 800 weight; `true`/`false`), or per-row control (RM-122) — see {@link WaterfallLabelsConfig}. Default `true`. |
grid | boolean | The 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`. |
positiveFill | string | Fill for an increasing step. Default `var(--chart-1)`. |
negativeFill | string | Fill for a decreasing step. Default `var(--chart-2)`. |
totalFill | string | Fill for a `"total"` row. Default `var(--chart-foreground)`. |
unit | number | When 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. |
valueFormat | ChartValueFormat | Value/label format. Default `"compact"`. |
callouts | WaterfallCallout[] | The one or two steps that actually explain the bridge, named directly on the chart (see {@link WaterfallCallout}). Default: none. |
plotHeight | Responsive<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. |
height | number | @deprecated Use `plotHeight` (`height={n}` is an alias for `plotHeight={n}`); removed in 7.0.0. |
margin | number | Partial<Margin> | Chart margins: one number for every side, or per side. Default: 40 on every side. |
className | string | Additional class name for the container. |
status | ChartStatus | Loading 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"`. |
accessibleLabel | ChartA11yProps["accessibleLabel"] | Accessible name for the chart region. |
accessibleDescription | ChartA11yProps["accessibleDescription"] | Supplemental description read by AT. |
annotations | readonly 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`. |
analytics | readonly 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. |
palette | ChartPalette | Colour 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. |
onDatapointClick | ChartDatapointClickHandler<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. |
copyValueOnActivate | boolean | Put 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. |
datapointLabel | ChartDatapointLabel<WaterfallStep> | Override the accessible name of each keyboard target. |
maxInteractiveDatapoints | number | Dev-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. |
selectionStates | ChartSelectionStatesResolver<WaterfallRow> | Maps a mark (category, optional series key, optional raw row) to its selection state. Unset → the chart renders exactly as before. |
dimExcluded | boolean | Dim `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. |
Grid
| Prop | Type | Description |
|---|---|---|
mode | GridMode | Grid painting mode (RM-108). Default: `"lines"`. |
horizontal | boolean | Show horizontal grid lines. Default: true |
vertical | boolean | Show vertical grid lines. Default: false |
numTicksRows | number | Number 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). |
numTicksColumns | number | Number of vertical grid lines. Default: 10 |
rowTickValues | number[] | Explicit tick values for horizontal grid lines. Overrides numTicksRows. |
stroke | string | Grid line stroke color. Default: var(--chart-grid) |
loadingStroke | string | Grid stroke while loading chrome is active. Falls back to `stroke`. |
strokeOpacity | number | Grid line stroke opacity. Default: 1 |
strokeWidth | number | Grid line stroke width. Default: `CHART_HAIRLINE_WIDTH` — the one furniture weight. |
strokeDasharray | string | Grid line dash array. Default: "4,4" for dashed lines |
highlightRowValues | number[] | Horizontal row values rendered with alternate styling (e.g. zero baseline). |
highlightRowStroke | string | Stroke for highlighted rows. Default: var(--chart-foreground-muted) |
highlightRowStrokeOpacity | number | Stroke opacity for highlighted rows. Default: 1 |
highlightRowStrokeWidth | number | Stroke width for highlighted rows. Default: 1 |
highlightRowStrokeDasharray | string | Dash array for highlighted rows. Default: solid line |
highlightRowLabel | (value: number) => string | undefined | Text 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. |
highlightColumnValues | Array<number | Date> | Vertical reference lines at these `xDataKey`-domain values (e.g. "today" on a forecast chart) — the column counterpart of `highlightRowValues`. Default: none. |
highlightColumnStroke | string | Stroke for highlighted columns. Default: var(--chart-foreground-muted) |
highlightColumnStrokeOpacity | number | Stroke opacity for highlighted columns. Default: 1 |
highlightColumnStrokeWidth | number | Stroke width for highlighted columns. Default: 1 |
highlightColumnStrokeDasharray | string | Dash array for highlighted columns. Default: solid line |
highlightColumnLabel | (value: number | Date) => string | undefined | Text 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. |
fadeHorizontal | boolean | Enable horizontal fade effect on grid rows (fades at left/right). Default: true |
fadeVertical | boolean | Enable vertical fade effect on grid columns (fades at top/bottom). Default: false |
yAxisId | string | number | Y-scale for horizontal grid lines. Default: primary (`"left"`) axis. |
shimmer | boolean | Animate a shimmer band across horizontal grid lines. Default: false |
shimmerStroke | string | Shimmer band stroke (color and opacity via color-mix or oklch alpha). |
shimmerLength | number | Shimmer band width in pixels. Default: 140 |
shimmerSpeed | number | Shimmer speed multiplier (higher = faster). Default: 1 |
shimmerSync | boolean | Match loop timing to the loading line pulse (cycle + inter-loop pause). |
BarXAxis
| Prop | Type | Description |
|---|---|---|
tickerHalfWidth | number | Width of the date ticker box for fade calculation. Default: 50 |
showAllLabels | boolean | Keep 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. |
maxLabels | number | Maximum number of labels to show. Default: 12 |
fit | CategoryAxisFit | How 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. |
title | ReactNode | Axis title — names what the categories are (RM-188; the same part `XAxis` draws). |
titlePlacement | AxisTitlePlacement | `"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
| Prop | Type | Description |
|---|---|---|
showAllLabels | boolean | Keep 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. |
maxLabels | number | Maximum number of labels to show. Default: 20 |
fit | CategoryAxisFit | `"off"` pins the pre-fit behaviour — full labels, count-capped stride, no measurement and no reserved gutter. The regression escape hatch. Default: `"auto"`. |
maxWidth | number | The 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. |
title | ReactNode | Axis title — names what the rows are (RM-188; the same part `YAxis` draws). |
titlePlacement | AxisTitlePlacement | `"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
| Prop | Type | Description |
|---|---|---|
showDatePill | boolean | Whether 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. |
showCrosshair | boolean | Whether to show the vertical crosshair line. Default: true |
showDots | boolean | Whether to show dots on the lines. Default: true |
indicatorColor | string | ((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.ReactNode | Custom content renderer for the tooltip box |
rows | (point: Record<string, unknown>) => TooltipRow[] | Custom row renderer - return array of TooltipRow |
variant | ChartTooltipVariant | Box 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. |
focus | boolean | Drive 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. |
pin | boolean | Tap-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. |
valueInTitle | boolean | Swap 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. |
dotColor | string | ((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. |
children | React.ReactNode | Additional content to show below rows (e.g., markers) |
className | string | Custom class name |
springConfig | SpringConfig | Per-chart override for the crosshair / dot / date-pill spring. |
boxSpringConfig | SpringConfig | Per-chart override for the floating-panel spring. |
panelStyle | React.CSSProperties | Inline styles for the tooltip panel (background, blur, etc.). |
unit | string | Unit 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. |
valueFormat | ChartValueFormat | STYLE 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. |
currency | string | ISO 4217 code for `valueFormat`'s `style: "currency"`. |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| connector | var(--chart-foreground-muted), 0.6px, dashed (furniture, not data — Leader's default) |
| decrease | var(--chart-2) (default negativeFill) |
| increase | var(--chart-1) (default positiveFill) |
| total | var(--chart-foreground) (default totalFill) |
| valueLabel | var(--chart-foreground) on a var(--chart-background) halo (HaloText) |