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.jsonUsage
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
- One shared time axis. Every bar is positioned by percent against the same denominator. If
windowMsis provided, that's the denominator — useful when bars are sliding across a stable canvas. Otherwise the denominator is the lateststart + durationacross all entries, with a 1ms floor so an empty strip still renders without dividing by zero. - 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.
AnimatePresencewithmode="popLayout". Rows enter, exit, and reorder with motion-layout transitions, so swappingentriesin place reads as a smooth shift rather than a flicker.- Cached state is data, not a className toggle. Mark an entry
cachedand the row picks updata-state="cached", swaps the bar fill for a muted track, and renders a cache glyph at the bar's start edge. The duration column shows0instead of the original number. - Token-driven styling. Bar fills resolve to
var(--cb-accent)by default; passcolorper entry to recolour individual rows from CSS variables. The track sits onbg-cb-bg-mutedso it adapts to light, dark, and high-contrast themes without component changes. - Reduced motion.
usePrefersReducedMotion()short-circuits entry/exit transitions and layout slides to instant.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
entries | readonly WaterfallStripEntry[] | required | Rows in render order — each carries id, label, start, duration, optional color, optional cached. |
windowMs | number | — | Fixed time-axis denominator. Falls back to the latest start + duration when omitted. |
title | string | — | Caption rendered above the strip. |
hint | string | — | Sub-caption rendered to the right of the title. |
hideDurations | boolean | false | When true, the per-row duration column is hidden. |
className | string | — | Merged onto the outer <div> via cn(). |
Accessibility
- The outer container is
role="figure"with anaria-labelsummarising 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-hiddenbecause 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 customBundleStateshape, computed initial vs. lazy chunk rows from astate.chunks[].chunk.rolediscriminator, 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 flatentries[]list withstartandduration, exposes per-rowcachedandcoloras data rather than derived state, swaps the CSS-modules stylesheet for token-driven Tailwind classes, and adds an optionalwindowMsso callers can pin the time axis when animating bars across a stable canvas.