TL;DR Render one bar per day from an array of { date, uptime } where uptime can be null for days with no data. Color by threshold, give every bar an aria-label and a keyboard-reachable tooltip, show 30 days on phones and 90 on desktop, stagger the bars in with one CSS variable, and compute the headline percentage only over days that have data.
The row of thin green bars on a status page is one of the most copied patterns in dashboards. Each bar is a day, a red one is an incident, and the number at the end says how reliable the service has been. Ask Cursor or v0 for it and you usually get 90 divs with random colors, no way to see what a bar means, and an uptime number that counts missing days as perfect. I make animated React components at qodesign, including an ops dashboard with a service map, and this is how I build the strip.
That is the Orbit Dashboard, a free and MIT-licensed ops dashboard on qodesign. The uptime strip below fits into a screen like it.
One entry per day, oldest first. uptime is a fraction from 0 to 1, or null when you have no data for that day (the monitor didn't exist yet, or the check itself was down):
ts
export type Day = { date: string; // "2026-10-21" uptime: number | null; incidents?: string[];};
Keeping null separate from 1 is the most important decision in this component. A day with no data isn't a perfect day.
export function barColor(uptime: number | null) { if (uptime === null) return "bg-zinc-700"; if (uptime >= 0.999) return "bg-emerald-500"; if (uptime >= 0.99) return "bg-amber-400"; return "bg-rose-500";}export function overallUptime(days: Day[]) { const known = days.filter((d) => d.uptime !== null) as (Day & { uptime: number })[]; if (known.length === 0) return null; return known.reduce((sum, d) => sum + d.uptime, 0) / known.length;}const pct = new Intl.NumberFormat("en-US", { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 3 });
Pick thresholds that match your SLA. With these, a day with about 1.4 minutes of downtime is still green, and a day under 99% (more than about 14 minutes down) is red. Averaging daily fractions is fine when each day is checked equally often; if check intervals vary, sum the minutes up and the minutes checked instead.
Each bar is a button, so keyboard users can Tab through the days and hear each label. An ordered list tells screen readers the days are in sequence.
flex-1 with a 2px gap lets the strip fill any width. On phones the older 60 days are hidden with hidden md:flex, so 30 bars stay thick enough to tap.
Dates are parsed and printed in UTC. Your monitor almost certainly buckets days in UTC, and local time would shift the labels by a day for half the world.
The clamp() keeps the tooltip inside the card at both ends of the strip. On phones the hidden bars shift the math slightly; if that bothers you, compute the index within the visible range. The accessible tooltip post covers Escape to close and touch behavior in more depth.
Six milliseconds per bar puts the last of 90 bars at about half a second, plus the bar's own 400ms. The --i variable is set on the li and inherited by the button. In Tailwind v4, hover:scale-y-110 uses the separate scale property, so it doesn't fight the entrance animation over transform.
Most monitors (your own cron job, or a hosted uptime checker) can give you minutes down per day. Do the bucketing on the server, return exactly 90 entries with null for the gaps, and render the strip in a client component. Keep that endpoint cached; nobody needs per-second freshness on a 90-day history.
An uptime strip usually sits next to a service map, a latency chart and an incidents table. The Orbit Dashboard has all of those, animated and free under MIT, in the free kit with a PROMPT.md for Cursor, v0 and Lovable. To animate the rest of a generated dashboard, see how to animate a v0 dashboard.
Render one element per day from an array with the date and the uptime fraction, color each by threshold with Tailwind classes, and lay them out in a flex row with a small gap. Give each bar an aria-label with the date and percentage, and show details in a tooltip on hover and focus.
Average the daily uptime only over days that have data, and treat days without data as unknown instead of 100%. If check intervals differ between days, sum the minutes up and divide by the minutes checked.
Around 30. Ninety bars on a phone become hairlines that are hard to tap, so hide the older days below the md breakpoint and keep the label saying how far back the strip goes.
Give each bar its index as a CSS variable and use one keyframe that scales it up from the bottom with a delay of a few milliseconds per index. Turn the animation off under prefers-reduced-motion.