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.
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.
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.
visitsByService: {
title: 'Visits by service',
subtitle: 'last 30 days',
categories: [
{ label: 'Emergency', value: 4210 },
{ label: 'Orthopaedics', value: 2890 },
// add or remove freely — colors cycle
],
}
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;">...`; });
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.