Heartwood Health · How to Build

02 · Staggered Bars.

← components.html
all how-tos
02
Staggered Bars

Categorical comparison · vertical bars

The workhorse for comparing 4–8 categories on the same metric. More than that, switch to a horizontal bar — labels stop fitting under each column.

When to use

Comparing a count or magnitude across a small number of named categories. Service volume, channel breakdown, region totals. The colors cycle through --cat-1…--cat-6 automatically — six is the natural ceiling.

How it animates

Each bar transitions from height: 0 to its target height with a cubic-bezier ease over 0.9s. Stagger 80ms between columns so the eye reads left-to-right. Each bar's value label fades in synced to its bar's transition delay.

Data shape
visitsByService: {
  title: 'Visits by service',
  subtitle: 'last 30 days',
  categories: [
    { label: 'Emergency',    value: 4210 },
    { label: 'Orthopaedics', value: 2890 },
    // add or remove freely — colors cycle
  ],
}
Build pattern
const max = Math.max(1, ...cats.map(c => c.value));
const maxBarPx = 121; // 160 − value − label − gaps
cats.forEach((c, i) => {
  const h = Math.round((c.value / max) * maxBarPx);
  // emit --h in PX, not %, see callout
  col.innerHTML = `...<div class="bar" style="--h: ${h}px;">...`;
});
⚠ Watch

Use px, not %, for --h. The .bars parent uses align-items: flex-end, which content-sizes its flex columns. The bar-col's height is therefore auto, so a percentage on .bar resolves against an indefinite parent — Chromium computes that to 0px, and your bars never appear.

Pixel values bypass the percentage-resolution problem entirely. The cap of 121px is the available runway in the 160px-tall .bars container after subtracting the value label, the bar label, and the two flex gaps.