Waterfall Chart
A full network waterfall. Each row is one request, broken into its constituent phases — DNS, Connect, SSL, TTFB, Download by default, or any names you pass — so the slow part of every request is visible at a glance. Hover or focus a row to surface its full phase breakdown in the detail panel.
This is the more detailed sibling of WaterfallStrip (in dsa-viz/). Use WaterfallStrip when each row is a single solid bar; reach for WaterfallChart when the phase mix inside each bar is the story.
Preview
Page load waterfall
Each row breaks into its network phases — slow phases stand out.
hover or focus a row to surface its phase breakdown
4 requests over 800ms - 336KBdnsconnectsslttfbdownload
Customize
Document timing
80ms
45ms
800ms
Requests
Chrome
Installation
npx shadcn@latest add https://craftbits.dev/r/waterfall-chart.jsonUsage
import { WaterfallChart } from "@craft-bits/core";
<WaterfallChart
title="Page load waterfall"
windowMs={700}
entries={[
{
id: "document",
label: "document",
sizeKB: 32,
blocking: true,
phases: [
{ phase: "dns", start: 0, duration: 20 },
{ phase: "connect", start: 20, duration: 30 },
{ phase: "ssl", start: 50, duration: 25 },
{ phase: "ttfb", start: 75, duration: 80 },
{ phase: "download", start: 155, duration: 45 },
],
},
{
id: "vendor",
label: "vendor.js",
sizeKB: 184,
phases: [
{ phase: "ttfb", start: 210, duration: 60 },
{ phase: "download", start: 270, duration: 200 },
],
},
]}
/>Anatomy
- Header. Optional
title(rendered with thecb-labelstyle) and adescriptionsub-line. Omit both for a chromeless panel. - Ruler. Tick marks across the top of the chart, auto-sized to
windowMs. Hide withhideRuler. - Rows. Each entry renders as a focusable
<button>row. The left column is the request label; the centre is a positioned phase track; the right column is the optional size column. The active row highlights in the muted background and surfaces its breakdown in the detail panel. - Phase track. Each phase is an absolutely-positioned span coloured by the palette (or per-phase
color). Phases can overlap — the chart trusts yourstart/durationdata and never re-orders. - Blocking rail. Rows with
blocking: truepaint a 2px--cb-errorrail at the bar start and pick updata-cb-blocking="true"for theming. - Detail panel. Sits under the chart, reads "hover or focus a row to surface its phase breakdown" when empty, and lights up with the active row's label, range, size, and per-phase durations on hover/focus.
- Footer. A summary chip on the left ("4 requests over 0.7s — 336KB"), the phase legend on the right.
Understanding the component
- One shared time axis. Every phase positions itself by percent against the same denominator. If
windowMsis provided, that's the denominator; otherwise the denominator is the lateststart + durationacross every phase of every entry, floored at 1ms so an empty chart still renders. - Phases own their geometry. A row's
startandendare derived from the earliest and latest of its phases — there's no per-rowstart/durationto keep in sync. The legend reads every unique phase name across the whole chart so consumers can introduce custom phases ("queued","waiting","redirect") without configuring the chart. - Palette resolution order. A phase paints with (1) its own
coloroverride, (2) thephaseColorsprop, (3)WATERFALL_CHART_DEFAULT_PHASE_COLORS, then (4)var(--cb-accent). That means the default palette covers the canonical HTTP names out of the box, but custom phase names still render with the accent token without configuration. - Keyboard navigation. Arrow Up/Down and Left/Right move row focus; Home/End jump to the first/last row; Escape clears the active row. Each row is a
<button>with a rovingtabIndex, so a single Tab lands you on the table and then the arrows take over. - Reduced motion.
usePrefersReducedMotion()short-circuits enter/exit transitions and layout slides to instant — the chart snaps into place with no animation.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
entries | WaterfallChartEntry[] | required | Rows in render order. |
windowMs | number | inferred | Fixed time-axis denominator in ms. |
title | ReactNode | — | Heading above the chart. |
description | ReactNode | — | Sub-headline under the title. |
phaseColors | Record<string, string> | — | Override the per-phase colour palette. |
rowHeightPx | number | 26 | Height of each row in pixels. |
labelWidthPx | number | 120 | Width of the label column in pixels. |
hideRuler | boolean | false | Hide the ruler across the top. |
hideLegend | boolean | false | Hide the phase legend in the footer. |
headingAs | "h2" | "h3" | "h4" | "h3" | Tag for the title element. |
className | string | — | Merged onto the root via cn(). |
WaterfallChartEntry
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier — used as React key and ARIA hook. |
label | ReactNode | Row label rendered on the left. |
phases | WaterfallChartPhase[] | Ordered sub-phases of this request. |
blocking | boolean | When true, the row reads as render-blocking and picks up data-cb-blocking="true". |
sizeKB | number | Optional aggregate size rendered next to the bar. |
detail | ReactNode | Optional richer body for the hover/focus detail panel. |
WaterfallChartPhase
| Field | Type | Description |
|---|---|---|
phase | string | Canonical name (dns, connect, ssl, ttfb, download) or any custom label. |
start | number | Offset from the chart origin in ms. |
duration | number | Length of the phase in ms. |
color | string | Optional CSS colour override for this phase only. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="waterfall-chart". The row container is arole="list"with anaria-labelsummarising request count and timeline width. - Each row is a
<button role="listitem">with a descriptivearia-labelreading the request label, its blocking state, and its per-phase durations. Keyboard users tab to the table, then use Arrow keys (and Home/End) to walk the rows. - The detail panel is a
role="status" aria-live="polite"region, and is wired asaria-describedbyon the row container while a row is active. - Each phase span carries
data-cb-phase="<name>"so consumers can extend the palette via CSS without monkey-patching the component. - Phase identity is never communicated through colour alone — every phase name renders as text in the detail panel and the legend.
- Animations short-circuit under
prefers-reduced-motion.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-web-performance/ui/WaterfallChart.tsx). The original consumed a project-levelWaterfallResourceshape withstartMs/endMs/type/dependsOn/optimizedfields, drove its palette from a customRESOURCE_COLORSmap, used a CSS-modules stylesheet for layout, and rendered dependency arrows specific to the SDP web-performance lab. The library extract drops every lab-specific prop and chrome, generalises the API to a flatentries[]shape where each entry carries its ownphases[](so the slow part of every request is the story), swaps the CSS-modules stylesheet for token-driven Tailwind classes, and adds keyboard navigation withrole="list"semantics. - Sibling:
WaterfallStripindsa-viz/is the lighter-weight cousin — single solid bars per row, no phase breakdown, no detail panel. Reach forWaterfallStripfor "what happened when"; reach forWaterfallChartwhen "what happened inside this request" is the story.