Data Storytelling

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

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

18
6
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
itemsDurationCompareItem[]—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
minWidthnumber6Minimum width (percent) of the shortest bar, used by the broken and log scales
msPerPercentnumber18Grow time (ms) per 1% of width; every bar grows at the same speed, so long bars naturally take longer
maxGrownumber2400Upper 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)
classNamestring—Passed through to the outermost container

DurationCompareItem

FieldTypeDefaultDescription
idstring—Unique id, used as the React key
labelstring—Label on the left, e.g. "Traditional"
valuenumber—Duration value, in the unit given by unit
unit"min" | "h" | "d" | "w" | "mo""min"Unit of value; a month counts as 30 days
colorstring—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

VariableDefaultDescription
--wb-duration-compare-slow#a3a3a3Colour of the longer bars
--wb-duration-compare-fast#22c55eAccent colour of the shortest bar and the ratio badge
--wb-duration-compare-badge-duration260msPop-in duration of the ratio badge

How it works

  • Same-speed growth: each bar animates for final width × msPerPercent, capped at maxGrow; 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 the aria-label always show the true value, and the zigzag and "…" at the break make the omitted span explicit. Use scale="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's aria-label reads "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