Duration Compare Bars
A duration comparison React component: two to five durations grow on one shared scale at one shared speed — say 30 minutes versus six weeks. Units convert automatically, readouts count up with the bar width, a ratio badge pops in when they finish, and a broken axis keeps the shortest bar visible when the gap is extreme.
The most direct way to show "how much time this saves": lay out two to five durations as horizontal bars and let them grow together on one scale at one speed — the short bar finishes almost instantly, the long one keeps you waiting, and that wait is the point. Inputs are minutes or values with a unit (minutes / hours / days / weeks / months), displayed in the best-fitting unit automatically; the readout on the right counts from 0 to the true value as the bar grows, and a ratio badge pops in once every bar is done. When the gap is extreme (30 minutes versus 6 weeks is 2016×), the default broken scale cuts a zigzag into the middle of the long bar so the shortest one stays visible.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add duration-compare-bars.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<DurationCompareBars/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/duration-compare-bars.en.jsonOr, once registries are configured in components.json, install it as @webberui/duration-compare-bars.
Usage
import { DurationCompareBars } from "@/components/ui/duration-compare-bars";
<DurationCompareBars
items={[
{ id: "legacy", label: "Traditional", value: 6, unit: "w" },
{ id: "ours", label: "With us", value: 30 },
]}
/>
// Three bars, log scale, badge shows the percentage saved
<DurationCompareBars
scale="log"
ratio="percent"
items={[
{ id: "manual", label: "Manual", value: 2, unit: "d" },
{ id: "semi", label: "Semi-auto", value: 3, unit: "h" },
{ id: "auto", label: "Automated", value: 5 },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | DurationCompareItem[] | — | Durations to compare (2–5 recommended), stacked top to bottom |
scale | "linear" | "log" | "broken" | "broken" | Scale; broken adds an axis break to the long bar when the gap is extreme, so the shortest bar stays visible |
minWidth | number | 6 | Minimum width (percent) of the shortest bar, used by the broken and log scales |
msPerPercent | number | 18 | Grow time (ms) per 1% of width; every bar grows at the same speed, so long bars naturally take longer |
maxGrow | number | 2400 | Upper limit (ms) on a single bar's grow time |
ratio | "times" | "percent" | "none" | "times" | Badge shown once every bar has finished: "2016× faster", "99.9% less time", or hidden |
locale | "zh-TW" | "en" | "en" | Locale for unit words: "30 minutes" or "30 分鐘" (30 minutes); the Chinese registry version defaults to "zh-TW" |
trigger | "inView" | "mount" | "inView" | When the animation starts: on entering the viewport, or right after mount |
onComplete | () => void | — | Called once every bar has finished growing (when the badge pops in) |
className | string | — | Passed through to the outermost container |
DurationCompareItem
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id, used as the React key |
label | string | — | Label on the left, e.g. "Traditional" |
value | number | — | Duration value, in the unit given by unit |
unit | "min" | "h" | "d" | "w" | "mo" | "min" | Unit of value; a month counts as 30 days |
color | string | — | Custom bar colour; by default the shortest bar uses the accent colour and the rest use grey |
formatDuration(minutes, locale) is also exported by name, so you can reuse the same unit conversion elsewhere.
CSS variables
| Variable | Default | Description |
|---|---|---|
--wb-duration-compare-slow | #a3a3a3 | Colour of the longer bars |
--wb-duration-compare-fast | #22c55e | Accent colour of the shortest bar and the ratio badge |
--wb-duration-compare-badge-duration | 260ms | Pop-in duration of the ratio badge |
How it works
- Same-speed growth: each bar animates for final width ×
msPerPercent, capped atmaxGrow; the first 80% runs at constant speed and the last stretch eases out. Long bars take longer — that sense of waiting is exactly what the component is meant to convey. - Unit conversion: from largest to smallest, the first unit where the value is ≥ 1 and a multiple of 0.5 wins, so 6 weeks shows as "6 weeks" rather than "1.4 months", and 90 minutes shows as "1.5 hours". While counting, the unit steps up through minutes, hours and days, but never beyond the final value's unit.
- Honesty of the broken axis: broken only kicks in when linear widths would make the shortest bar narrower than
minWidth. Bars before the break are drawn to scale; bars that cross the break only guarantee order and longest = 100%, so their width is no longer proportional to value — which is why the readout on the right and thearia-labelalways show the true value, and the zigzag and "…" at the break make the omitted span explicit. Usescale="linear"when you need strict proportions. - Ratio and percent: ratio = longest ÷ shortest, rounded to an integer from 10× up; percent saved is floored to one decimal so 99.95% never rounds up to 100%.
Accessibility
- Bars are rendered with
role="list"; each bar'saria-labelreads "label: duration" (e.g. "Traditional: 6 weeks"), and the counting numbers are hidden from assistive tech - A screen-reader-only summary sits below the chart (e.g. "With us takes only 30 minutes, 2016× faster than Traditional.") and is linked with
aria-describedby - Readouts accompany colour, so fast and slow are never told apart by colour alone
- When the user's system has reduce motion enabled, bar widths and numbers jump straight to their final values and the ratio badge appears immediately
Route Map Card
A navigation-style route card — a grid basemap with deterministic streets, a route drawn segment by segment, a marker travelling along it, street labels fading in on arrival and a live ETA readout.
Diagram Kit
Eleven data-driven SVG diagrams — hub routing, sequence lanes, cycle, timeline, radial branches, funnel, tier pyramid, anchored callouts, branch reveal, quadrant and Venn — each drawn in with stroke tracing and staggered nodes.