Uptime Bar
NewInstallation
npx shadcn@latest add https://uibrik.com/r/uptime-bar.jsonInstall the packages
npm install @number-flow/react @tabler/icons-react motion react-use-measureAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/uptime-bar/uptime-bar.tsx"use client"; import { useState, type PointerEvent } from "react"; import { motion, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; import { Announce, ChartCard, DataTable, Legend, Tooltip, TooltipTitle, useChartCursor, useWidth } from "../_charts/parts"; import { clamp } from "../_charts/scale"; import { DRAW_IN, EASE_OUT, GAP, STATUS } from "../_charts/theme"; import { dayLabels } from "../_charts/sample"; export type UptimeStatus = "operational" | "degraded" | "outage" | "none"; export type UptimeDay = { /** Shown as written, e.g. "Oct 12". */ date: string; status: UptimeStatus; /** What happened, e.g. "Elevated error rates". */ note?: string; /** Minutes of downtime that day. */ downtime?: number; }; export type UptimeService = { name: string; days: UptimeDay[] }; const TICK_H = 28; // Narrowest a tick may get before older days give way. const TICK_MIN = 3; // Air above and below the ticks, so the hover target is taller than the mark. const PAD_Y = 4; const DAY_MINUTES = 1440; const STATUS_COLOR: Record<UptimeStatus, string> = { operational: STATUS.good, degraded: STATUS.warning, outage: STATUS.critical, none: STATUS.empty, }; const STATUS_LABEL: Record<UptimeStatus, string> = { operational: "Operational", degraded: "Degraded performance", outage: "Outage", none: "No data", }; const LEGEND = [ { key: "operational", label: "Operational", color: STATUS.good }, { key: "degraded", label: "Degraded", color: STATUS.warning }, { key: "outage", label: "Outage", color: STATUS.critical }, { key: "none", label: "No data", color: STATUS.empty }, ]; // ─── Sample ─────────────────────────────────────────────────────────────────── // Mostly quiet, with a few incidents placed by hand so the history reads like a // real status page: one shared database failover, and a webhooks service that // launched partway through the window. type Incident = [daysAgo: number, status: UptimeStatus, note: string, downtime: number]; function history(count: number, incidents: Incident[], launched = Infinity): UptimeDay[] { const byAgo = new Map(incidents.map((i) => [i[0], i])); return dayLabels(count).map((date, i) => { const ago = count - 1 - i; if (ago >= launched) return { date, status: "none" }; const hit = byAgo.get(ago); return hit ? { date, status: hit[1], note: hit[2], downtime: hit[3] } : { date, status: "operational" }; }); } const SAMPLE: UptimeService[] = [ { name: "API", days: history(90, [ [61, "degraded", "Elevated error rates", 14], [33, "outage", "Database failover", 47], [12, "degraded", "Slow responses in eu-west", 22], [4, "degraded", "Elevated latency", 9], ]), }, { name: "Dashboard", days: history(90, [ [74, "degraded", "Slow page loads", 31], [40, "degraded", "Charts failing to load", 18], [33, "outage", "Sign-in unavailable", 26], ]), }, { name: "Webhooks", days: history( 90, [ [52, "degraded", "Delayed deliveries", 38], [33, "degraded", "Retries queued", 41], [19, "outage", "Deliveries paused", 64], [2, "degraded", "Delayed deliveries", 12], ], 84, ), }, ]; // ─── Helpers ────────────────────────────────────────────────────────────────── /** Share of tracked minutes that were up; days with no data don't count. */ function uptime(days: readonly UptimeDay[]) { const tracked = days.filter((d) => d.status !== "none"); if (!tracked.length) return null; const down = tracked.reduce((s, d) => s + (d.downtime ?? 0), 0); return 1 - down / (tracked.length * DAY_MINUTES); } // Two decimals, never rounding a bad day up to a perfect 100%. const formatUptime = (u: number | null) => (u === null ? "No data" : `${(Math.floor(u * 10000) / 100).toFixed(2)}%`); const formatMinutes = (m: number) => (m < 60 ? `${m} min` : `${Math.floor(m / 60)} h${m % 60 ? ` ${m % 60} min` : ""}`); const detail = (d: UptimeDay) => [d.note, d.downtime ? formatMinutes(d.downtime) : undefined].filter(Boolean).join(" · "); const describe = (d: UptimeDay) => [STATUS_LABEL[d.status], detail(d)].filter(Boolean).join(", "); /** The worst status across services today, as the headline state. */ function overall(services: readonly UptimeService[]) { const today = services.map((s) => s.days[s.days.length - 1]?.status ?? "none"); if (today.includes("outage")) return { color: STATUS.critical, label: "Partial outage" }; if (today.includes("degraded")) return { color: STATUS.warning, label: "Degraded performance" }; if (today.every((s) => s === "none")) return { color: STATUS.empty, label: "No data" }; return { color: STATUS.good, label: "All systems operational" }; } // ─── Component ──────────────────────────────────────────────────────────────── export type UptimeBarProps = { /** Names the page. */ title?: string; /** One row per service, each with one entry per day, oldest first. */ services?: UptimeService[]; /** Most days to show; fewer fit on narrow screens. */ days?: number; /** Names the four states under the card. */ showLegend?: boolean; className?: string; }; export function UptimeBar({ title = "System status", services = SAMPLE, days = 90, showLegend = true, className }: UptimeBarProps) { const [ref, width] = useWidth(); // Only one row shows a tooltip at a time: whichever was touched last. const [owner, setOwner] = useState<number | null>(null); // As many recent days as fit at TICK_MIN each, up to `days`. const longest = Math.max(0, ...services.map((s) => s.days.length)); const wanted = Math.min(days, longest); const count = width ? Math.max(1, Math.min(wanted, Math.floor((width + GAP) / (TICK_MIN + GAP)))) : wanted; const rows = services.map((s) => ({ name: s.name, days: s.days.slice(-count) })); const state = overall(services); return ( <ChartCard title={title} aside={ <span className="flex h-6 items-center gap-1.5 rounded-full bg-foreground/5 px-2.5 text-xs font-medium text-foreground"> <span aria-hidden className="size-2 rounded-full" style={{ background: state.color }} /> {state.label} </span> } footer={showLegend ? <Legend items={LEGEND} mark="dot" className="-ml-2" /> : undefined} className={cn("max-w-160", className)} > <div ref={ref} className="flex flex-col gap-4"> {rows.map((row, r) => ( <ServiceRow key={row.name} name={row.name} days={row.days} width={width} owner={owner === r} onClaim={() => setOwner(r)} /> ))} <div className="-mt-2 flex justify-between text-[11px] font-medium tabular-nums text-muted-foreground"> <span>{count === 1 ? "Today" : `${count} days ago`}</span> <span>Today</span> </div> </div> <DataTable caption={`${title}, last ${count} days`} columns={["Date", ...rows.map((r) => r.name)]} rows={Array.from({ length: count }, (_, i) => [ rows.find((r) => r.days[i])?.days[i].date ?? "", ...rows.map((r) => (r.days[i] ? describe(r.days[i]) : "No data")), ])} /> </ChartCard> ); } function ServiceRow({ name, days, width, owner, onClaim, }: { name: string; days: UptimeDay[]; width: number; /** This row holds the card's one tooltip. */ owner: boolean; onClaim: () => void; }) { const reduce = useReducedMotion(); const cursor = useChartCursor(days.length); const active = owner ? cursor.active : null; const pct = formatUptime(uptime(days)); const n = days.length; const tick = n ? (width - (n - 1) * GAP) / n : 0; const step = tick + GAP; const onPointerMove = (e: PointerEvent<HTMLDivElement>) => { if (!step) return; onClaim(); const x = e.clientX - e.currentTarget.getBoundingClientRect().left; cursor.point(clamp(Math.floor((x + GAP / 2) / step), 0, n - 1)); }; const day = active !== null ? days[active] : undefined; return ( <div> <div className="mb-1.5 flex items-baseline justify-between gap-3"> <span className="truncate text-[13px] font-medium text-foreground">{name}</span> <span className="shrink-0 text-xs tabular-nums text-muted-foreground">{pct} uptime</span> </div> <div role="group" aria-label={`${name}, ${pct} uptime over ${n} days. Arrow keys move between days.`} className="relative -my-1 cursor-default touch-none rounded-md outline-none focus-visible:ring-2 focus-visible:ring-foreground/20" style={{ height: TICK_H + PAD_Y * 2 }} onPointerMove={onPointerMove} onPointerDown={onPointerMove} {...cursor.bind} onKeyDown={(e) => { onClaim(); cursor.bind.onKeyDown(e); }} onFocus={(e) => { onClaim(); cursor.bind.onFocus(e); }} > {width > 0 && ( // Wipes in from the oldest day, like a line drawing in. The final // inset is negative so a lifted tick and its focus ring aren't clipped. <motion.div aria-hidden className="absolute inset-x-0 flex" style={{ top: PAD_Y, height: TICK_H, gap: GAP }} initial={{ clipPath: reduce ? "inset(-20% -2% -20% -2%)" : "inset(-20% 102% -20% -2%)" }} animate={{ clipPath: "inset(-20% -2% -20% -2%)" }} transition={{ duration: reduce ? 0 : DRAW_IN, ease: EASE_OUT }} > {days.map((d, i) => { const on = active === i; return ( <motion.div key={i} className={cn( "relative h-full flex-1 rounded-[2px] transition-opacity duration-150", active !== null && !on && "opacity-60", )} style={{ background: STATUS_COLOR[d.status] }} initial={false} animate={{ scaleY: on && !reduce ? 1.15 : 1 }} transition={{ duration: reduce ? 0 : 0.12, ease: EASE_OUT }} > {on && cursor.keyboard && <span className="absolute -inset-0.5 rounded-[4px] ring-2 ring-foreground/40" />} </motion.div> ); })} </motion.div> )} <Tooltip x={active !== null ? active * step + tick / 2 : 0} y={PAD_Y - 2} bounds={width} visible={!!day}> {day && ( <> <TooltipTitle>{day.date}</TooltipTitle> <div className="mt-1 flex items-center gap-1.5"> <span className="size-1.5 shrink-0 rounded-full" style={{ background: STATUS_COLOR[day.status] }} /> {STATUS_LABEL[day.status]} </div> {detail(day) && <div className="mt-0.5 opacity-60">{detail(day)}</div>} </> )} </Tooltip> </div> <Announce>{day ? `${name}, ${day.date}: ${describe(day)}` : ""}</Announce> </div> ); }
Adds the files to
components/uibrik/uptime-bar and installs @number-flow/react, @tabler/icons-react, motion and react-use-measure. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/uptime-bar.Props
UptimeBar- title
- Type string
- "System status"
- Names the page.
- days
- Type number
- 90
- Most days to show; fewer fit on narrow screens.
- showLegend
- Type boolean
- true
- Names the four states under the card.
- services
- Type { name: string; days: { date: string; status: "operational" | "degraded" | "outage" | "none"; note?: string; downtime?: number }[] }[]
- –
- One row per service, each with one entry per day, oldest first. Downtime is in minutes.
- className
- Type string
- –
- Classes for the outer shell.