Shadcn Waterfall Chart

Walk a starting value through its increases and decreases to a final total, with floating bars colored by direction.

CSV, TSV from Excel/Sheets, a Markdown table, or JSON.

CSV
Show
Code
IncreaseDecreaseTotal
Image
chart.tsx
"use client"

import { Bar, BarChart, CartesianGrid, LabelList, Rectangle, ReferenceLine, XAxis, YAxis } from "recharts"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
} from "@/components/ui/chart"

const data = [
  { Step: "Starting MRR", Change: 42000 },
  { Step: "New business", Change: 8600 },
  { Step: "Expansion", Change: 3200 },
  { Step: "Contraction", Change: -1400 },
  { Step: "Churn", Change: -2900 },
]

const chartConfig = {
  increase: { label: "Increase", color: "var(--chart-3)" },
  decrease: { label: "Decrease", color: "var(--chart-2)" },
  total: { label: "Total", color: "var(--chart-1)" },
} satisfies ChartConfig

type WaterfallStep = {
  label: string
  change: number
  end: number
  range: [number, number]
  kind: keyof typeof chartConfig
  display: string
}

const compact = (value: number) =>
  new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(value)

/** The first row is the starting total; every later row changes the running
 * total. Each step becomes a floating bar spanning [low, high]. */
function toSteps(rows: typeof data): WaterfallStep[] {
  const steps = rows.reduce<WaterfallStep[]>((acc, row, index) => {
    const value = row.Change ?? 0
    const previous = acc.at(-1)?.end ?? 0
    const start = index === 0 ? 0 : previous
    const end = index === 0 ? value : previous + value
    const kind = index === 0 ? "total" : value >= 0 ? "increase" : "decrease"
    return [
      ...acc,
      {
        label: String(row.Step),
        change: value,
        end,
        range: [Math.min(start, end), Math.max(start, end)],
        kind,
        display: kind !== "total" && value > 0 ? `+${compact(value)}` : compact(value),
      },
    ]
  }, [])
  const last = steps.at(-1)
  if (!last || steps.length < 2) return steps
  return [
    ...steps,
    { label: "Total", change: last.end, end: last.end, range: [Math.min(0, last.end), Math.max(0, last.end)], kind: "total", display: compact(last.end) },
  ]
}

function WaterfallTooltip({ active, payload }: { active?: boolean; payload?: ReadonlyArray<{ payload?: unknown }> }) {
  const step = payload?.[0]?.payload as WaterfallStep | undefined
  if (!active || !step) return null
  return (
    <div className="grid min-w-36 gap-1 rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs shadow-xl">
      <span className="font-medium text-foreground">{step.label}</span>
      <div className="flex items-center justify-between gap-4">
        <span className="flex items-center gap-1.5 text-muted-foreground">
          <span className="size-2 shrink-0 rounded-[2px]" style={{ backgroundColor: chartConfig[step.kind].color }} />
          {step.kind === "total" ? "Total" : "Change"}
        </span>
        <span className="font-mono font-medium text-foreground tabular-nums">
          {step.kind !== "total" && step.change > 0 ? "+" : ""}
          {step.change.toLocaleString("en-US")}
        </span>
      </div>
      {step.kind !== "total" && (
        <div className="flex items-center justify-between gap-4 text-muted-foreground">
          <span>Running total</span>
          <span className="font-mono tabular-nums">{step.end.toLocaleString("en-US")}</span>
        </div>
      )}
    </div>
  )
}

export function Chart() {
  const steps = toSteps(data)
  const hasNegative = steps.some((step) => step.range[0] < 0)

  return (
    <div className="flex w-full flex-col gap-2">
      <ChartContainer config={chartConfig} className="aspect-auto h-[330px] w-full">
        <BarChart accessibilityLayer data={steps} barCategoryGap="24%" margin={{ top: 20 }}>
          <CartesianGrid vertical={false} strokeDasharray="3 5" />
          <XAxis dataKey="label" tickLine={false} axisLine={false} tickMargin={10} />
          <YAxis tickLine={false} axisLine={false} tickMargin={8} width={44} tickFormatter={compact} />
          {hasNegative && <ReferenceLine y={0} stroke="var(--border)" />}
          <ChartTooltip cursor={{ fill: "var(--muted)", opacity: 0.6 }} content={<WaterfallTooltip />} />
          <Bar
            dataKey="range"
            fill="var(--chart-1)"
            radius={4}
            maxBarSize={56}
            shape={(props) => <Rectangle {...props} fill={chartConfig[(props.payload as WaterfallStep).kind].color} />}
          >
            <LabelList dataKey="display" position="top" offset={6} className="fill-foreground" fontSize={12} />
          </Bar>
        </BarChart>
      </ChartContainer>
      <div className="flex items-center justify-center gap-4 text-xs text-muted-foreground">
        {(Object.keys(chartConfig) as (keyof typeof chartConfig)[]).map((kind) => (
          <span key={kind} className="flex items-center gap-1.5">
            <span className="size-2 shrink-0 rounded-[2px]" style={{ backgroundColor: chartConfig[kind].color }} />
            {chartConfig[kind].label}
          </span>
        ))}
      </div>
    </div>
  )
}

When to use a waterfall chart

Waterfall (bridge) charts explain how you got from one number to another.

  • MRR bridge: new, expansion, contraction, churn
  • Profit and loss from revenue to net income
  • Budget vs. actual variance

Data format

  • Use 2 columns: Step,Change. The first row is the starting total.
  • Every later row is a change: positive for increases, negative for decreases.
  • A final Total bar is added for you (you can turn it off).
  • Paste CSV, JSON from your API, TSV copied from Excel or Google Sheets, or a Markdown table. The generated component keeps your column names as its data keys.
CSV
Step,Change
Starting MRR,42000
New business,8600
Expansion,3200
Contraction,-1400
Churn,-2900
Same data as JSON
[
  {"step": "Starting MRR", "change": 42000},
  {"step": "New business", "change": 8600},
  {"step": "Expansion", "change": 3200},
  …
]

Options

Total bar
Append a final bar with the ending total.
Values
Label each bar with its signed change or total.
Colors
Separate colors for increases, decreases, and totals.

How to add it to your project

  1. 1. Add the shadcn/ui chart component (it installs Recharts):
    npx shadcn@latest add chart
  2. 2. Paste your data above, pick the options, and copy the generated chart.tsx.
  3. 3. Save it in your project (e.g. components/waterfall-chart.tsx) and render <Chart />, or <Chart data={rows} /> in Data as prop mode.

Waterfall Chart FAQ