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.json

Usage

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 the cb-label style) and a description sub-line. Omit both for a chromeless panel.
  • Ruler. Tick marks across the top of the chart, auto-sized to windowMs. Hide with hideRuler.
  • 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 your start/duration data and never re-orders.
  • Blocking rail. Rows with blocking: true paint a 2px --cb-error rail at the bar start and pick up data-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

  1. One shared time axis. Every phase positions itself by percent against the same denominator. If windowMs is provided, that's the denominator; otherwise the denominator is the latest start + duration across every phase of every entry, floored at 1ms so an empty chart still renders.
  2. Phases own their geometry. A row's start and end are derived from the earliest and latest of its phases — there's no per-row start/duration to 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.
  3. Palette resolution order. A phase paints with (1) its own color override, (2) the phaseColors prop, (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.
  4. 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 roving tabIndex, so a single Tab lands you on the table and then the arrows take over.
  5. Reduced motion. usePrefersReducedMotion() short-circuits enter/exit transitions and layout slides to instant — the chart snaps into place with no animation.

Props

PropTypeDefaultDescription
entriesWaterfallChartEntry[]requiredRows in render order.
windowMsnumberinferredFixed time-axis denominator in ms.
titleReactNodeHeading above the chart.
descriptionReactNodeSub-headline under the title.
phaseColorsRecord<string, string>Override the per-phase colour palette.
rowHeightPxnumber26Height of each row in pixels.
labelWidthPxnumber120Width of the label column in pixels.
hideRulerbooleanfalseHide the ruler across the top.
hideLegendbooleanfalseHide the phase legend in the footer.
headingAs"h2" | "h3" | "h4""h3"Tag for the title element.
classNamestringMerged onto the root via cn().

WaterfallChartEntry

FieldTypeDescription
idstringStable identifier — used as React key and ARIA hook.
labelReactNodeRow label rendered on the left.
phasesWaterfallChartPhase[]Ordered sub-phases of this request.
blockingbooleanWhen true, the row reads as render-blocking and picks up data-cb-blocking="true".
sizeKBnumberOptional aggregate size rendered next to the bar.
detailReactNodeOptional richer body for the hover/focus detail panel.

WaterfallChartPhase

FieldTypeDescription
phasestringCanonical name (dns, connect, ssl, ttfb, download) or any custom label.
startnumberOffset from the chart origin in ms.
durationnumberLength of the phase in ms.
colorstringOptional CSS colour override for this phase only.

Accessibility

  • The wrapper is a <section> with data-cb-edu="waterfall-chart". The row container is a role="list" with an aria-label summarising request count and timeline width.
  • Each row is a <button role="listitem"> with a descriptive aria-label reading 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 as aria-describedby on 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-level WaterfallResource shape with startMs/endMs/type/dependsOn/optimized fields, drove its palette from a custom RESOURCE_COLORS map, 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 flat entries[] shape where each entry carries its own phases[] (so the slow part of every request is the story), swaps the CSS-modules stylesheet for token-driven Tailwind classes, and adds keyboard navigation with role="list" semantics.
  • Sibling: WaterfallStrip in dsa-viz/ is the lighter-weight cousin — single solid bars per row, no phase breakdown, no detail panel. Reach for WaterfallStrip for "what happened when"; reach for WaterfallChart when "what happened inside this request" is the story.