Waterfall Strip

A horizontal network-style waterfall. Pass a list of entries with start + duration along a shared time axis and the component lays them out as parallel bars stacked top-to-bottom. Optimised for HTTP request waterfalls, RUM traces, build pipelines, and any narrative where "what happens when, and for how long" is the story.

Network waterfallFirst visit
document
120
Customize
Strip
6
500ms
State

Installation

npx shadcn@latest add https://craftbits.dev/r/waterfall-strip.json

Usage

import { WaterfallStrip } from "@craft-bits/core";
 
const entries = [
  { id: "html", label: "document", start: 0, duration: 120 },
  { id: "vendor", label: "vendor.js", start: 40, duration: 240 },
  { id: "route", label: "route.js", start: 60, duration: 180 },
  { id: "lazy", label: "lazy.js", start: 320, duration: 140 },
];
 
<WaterfallStrip entries={entries} title="Network waterfall" />

Fixed time axis — keeps bar geometry stable while you animate entries over time:

<WaterfallStrip entries={entries} windowMs={500} />

Cached row — greys the bar and renders a cache glyph instead of a duration:

<WaterfallStrip
  entries={[
    { id: "vendor", label: "vendor.js", start: 0, duration: 240, cached: true },
    { id: "route", label: "route.js", start: 40, duration: 180 },
  ]}
/>

Understanding the component

  1. One shared time axis. Every bar is positioned by percent against the same denominator. If windowMs is provided, that's the denominator — useful when bars are sliding across a stable canvas. Otherwise the denominator is the latest start + duration across all entries, with a 1ms floor so an empty strip still renders without dividing by zero.
  2. Bars never get narrower than a hairline. Sub-2% bars are clamped up to 2% so the row remains visible at any zoom level. The clamp respects the strip edge — a bar near 100% gets clipped to fit rather than overshooting.
  3. AnimatePresence with mode="popLayout". Rows enter, exit, and reorder with motion-layout transitions, so swapping entries in place reads as a smooth shift rather than a flicker.
  4. Cached state is data, not a className toggle. Mark an entry cached and the row picks up data-state="cached", swaps the bar fill for a muted track, and renders a cache glyph at the bar's start edge. The duration column shows 0 instead of the original number.
  5. Token-driven styling. Bar fills resolve to var(--cb-accent) by default; pass color per entry to recolour individual rows from CSS variables. The track sits on bg-cb-bg-muted so it adapts to light, dark, and high-contrast themes without component changes.
  6. Reduced motion. usePrefersReducedMotion() short-circuits entry/exit transitions and layout slides to instant.

Props

PropTypeDefaultDescription
entriesreadonly WaterfallStripEntry[]requiredRows in render order — each carries id, label, start, duration, optional color, optional cached.
windowMsnumberFixed time-axis denominator. Falls back to the latest start + duration when omitted.
titlestringCaption rendered above the strip.
hintstringSub-caption rendered to the right of the title.
hideDurationsbooleanfalseWhen true, the per-row duration column is hidden.
classNamestringMerged onto the outer <div> via cn().

Accessibility

  • The outer container is role="figure" with an aria-label summarising entry count.
  • Each row's label renders as text — bar identity is never communicated through colour alone.
  • Cached rows surface as both a colour shift and a structural data attribute (data-state="cached") so emphasis survives high-contrast and grayscale rendering.
  • The cache glyph is aria-hidden because the row's text label and duration already convey the same information to screen readers.
  • Motion respects prefers-reduced-motion: row enter/exit and layout slides collapse to instant.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-bundle/ui/WaterfallStrip.tsx). The original was scoped to the bundle lab — it consumed a custom BundleState shape, computed initial vs. lazy chunk rows from a state.chunks[].chunk.role discriminator, hard-coded a stage threshold to flip into "warm cache" mode, and used a CSS-modules stylesheet for layout. The library extract generalises the API to a flat entries[] list with start and duration, exposes per-row cached and color as data rather than derived state, swaps the CSS-modules stylesheet for token-driven Tailwind classes, and adds an optional windowMs so callers can pin the time axis when animating bars across a stable canvas.