Uptime bar
A day-by-day uptime strip with a tooltip on every bar and the percentage at the end of the label row.
No hooks of its own, so it renders on the server, and it never reads the clock: every date comes from the days you pass, and they are read in UTC unless you say otherwise, which is what keeps the server and the client rendering the same strip wherever the reader is sitting. computeUptime is exported and pure — an up day counts one, a degraded day a half, a down day nothing, and a day with no data is left out of the reckoning entirely, so a gap in the record cannot drag the number down. The tooltip only reaches a pointer, so each bar also carries its date and status as an aria-label and the strip carries a summary of the whole run. showLegend renders the first and last day of the run under the strip — the range, not a colour key: what each colour means is already on every bar, in its label and its tooltip.
Install
npx shadcn@latest add @vibra/uptime-barNeeds the @vibra registry in your components.json — set it up once.
Examples
import { UptimeBar, type UptimeDay } from "@/components/ui/uptime-bar"
// A fixed last day, so all ninety dates are the same on the server and in the
// browser. Nothing in this file reads the clock.
const LAST_DAY = Date.UTC(2026, 8, 3, 12)
const DAY_MS = 86_400_000
// Index 0 is the oldest day. Everything not listed here stayed up.
const NOTABLE: Record<number, { status: UptimeDay["status"]; label: string }> = {
22: { status: "degraded", label: "Elevated p99 on the read replicas" },
55: { status: "down", label: "41m outage — expired TLS certificate" },
56: { status: "degraded", label: "Recovering, queues draining" },
78: { status: "degraded", label: "Slow warehouse queries" },
}
const DAYS: UptimeDay[] = Array.from({ length: 90 }, (_, index) => ({
date: new Date(LAST_DAY - (89 - index) * DAY_MS),
status: NOTABLE[index]?.status ?? "up",
label: NOTABLE[index]?.label,
}))
export default function UptimeBarDemo() {
return (
<div className="w-full max-w-lg rounded-lg border p-4">
<UptimeBar days={DAYS} label="api.vibra.example" />
</div>
)
}Compact, one row per service
The small size with the legend off, stacked into a service list.
import { UptimeBar, type UptimeDay } from "@/components/ui/uptime-bar"
// Fixed, so every date is identical on the server and in the browser.
const LAST_DAY = Date.UTC(2026, 8, 3, 12)
const DAY_MS = 86_400_000
// Index 0 is the oldest of the thirty days; anything unlisted stayed up.
function thirtyDays(notable: Record<number, UptimeDay["status"]>): UptimeDay[] {
return Array.from({ length: 30 }, (_, index) => ({
date: new Date(LAST_DAY - (29 - index) * DAY_MS),
status: notable[index] ?? "up",
}))
}
const SERVICES = [
{ name: "api.vibra.example", days: thirtyDays({}) },
{ name: "app.vibra.example", days: thirtyDays({ 11: "degraded" }) },
{ name: "ingest-worker", days: thirtyDays({ 25: "down", 26: "degraded", 27: "degraded" }) },
]
export default function UptimeBarCompact() {
return (
<div className="flex w-full max-w-lg flex-col divide-y rounded-lg border">
{SERVICES.map((service) => (
<div key={service.name} className="px-4 py-3">
<UptimeBar
days={service.days}
size="sm"
showLegend={false}
// A plain string, so the strip's accessible summary names the service.
label={service.name}
/>
</div>
))}
</div>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| days | { date: string | Date; status: "up" | "degraded" | "down" | "none"; label?: string }[] | — | One bar each, oldest first; none is a gap in the record, not an outage. |
| days[].date | string | Date | — | The day the bar stands for; also the first line of its tooltip. |
| days[].status | "up" | "degraded" | "down" | "none" | — | Colours the bar and sets what it weighs: one, a half, nothing, or nothing counted. |
| labelText | string | — | Summarises the run when label is markup rather than a string; the group's accessible name is built from it. |
| timeZone | string | "UTC" | IANA zone the dates are read in. A bar stands for a whole day, and those days come out of a database in UTC; pass the reader's zone only when the days are local ones. |
| days[].label | string | — | Detail read after the status — "2 incidents", "14m of downtime". |
| uptime | number | computeUptime(days) | A percentage measured somewhere more authoritative than a daily rollup. |
| label | React.ReactNode | — | What the strip is measuring; pass a string and it names the strip for a screen reader too. |
| showLegend | boolean | true | Puts the first and last day of the run under the strip. |
| size | "sm" | "default" | "default" | sm drops the bars to h-6 and the label row to text-xs. |
| computeUptime | (days: UptimeDay[]) => number | — | The percentage of measured days that stayed up; 100 when nothing was measured. |
Dependencies
Source
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { formatDate, percentOf } from "@/lib/format"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
const uptimeBarDayVariants = cva("min-w-0 flex-1 rounded-[2px]", {
variants: {
status: {
up: "bg-success",
degraded: "bg-warning",
down: "bg-danger",
// Not a good day and not a bad one — the track colour, so a gap in the
// record reads as a gap rather than as an outage.
none: "bg-muted",
},
size: {
default: "h-8",
sm: "h-6",
},
},
defaultVariants: { status: "up", size: "default" },
})
/** How one day went: up, degraded, down, or never measured. */
export type UptimeDayStatus = NonNullable<VariantProps<typeof uptimeBarDayVariants>["status"]>
/** How tall the strip stands. */
export type UptimeBarSize = NonNullable<VariantProps<typeof uptimeBarDayVariants>["size"]>
// A day with no data has no weight at all, which is what keeps it out of the
// arithmetic below rather than being counted as either a good day or a bad one.
const DAY_WEIGHTS: Partial<Record<UptimeDayStatus, number>> = { up: 1, degraded: 0.5, down: 0 }
const DAY_LABELS: Record<UptimeDayStatus, string> = {
up: "Operational",
degraded: "Degraded",
down: "Down",
none: "No data",
}
export type UptimeDay = {
date: string | Date
status: UptimeDayStatus
/** Detail for the day, read after its status — "2 incidents", "14m of downtime". */
label?: string
}
/** The share of measured days that stayed up, as a percentage: a degraded day counts half, a day with no data not at all, and nothing measured reads 100. */
export function computeUptime(days: UptimeDay[]): number {
let counted = 0
let earned = 0
for (const day of days) {
const weight = DAY_WEIGHTS[day.status]
if (weight === undefined) continue
counted += 1
earned += weight
}
if (counted === 0) return 100
return percentOf(earned, counted)
}
// The way a status page writes it: whole when the number is whole, two decimals
// when it is not, because the difference between 99.99 and 100 is the story.
function uptimeText(percent: number): string {
return Number.isInteger(percent) ? `${percent}%` : `${percent.toFixed(2)}%`
}
export type UptimeBarProps = React.ComponentProps<"div"> & {
days: UptimeDay[]
/** Overrides the computed percentage, for a figure measured somewhere more authoritative. */
uptime?: number
label?: React.ReactNode
/** Summarises the run when `label` is markup rather than a string. */
labelText?: string
/**
* IANA zone the dates are read in. A bar stands for a whole day and those
* days come out of a database in UTC, so UTC is what decides which day a
* bar is — read in the reader's own zone, a strip drifts a day west of
* Greenwich. Pass the reader's zone only when the days are local ones.
*/
timeZone?: string
/** Puts the first and last day of the run under the strip. */
showLegend?: boolean
size?: UptimeBarSize
}
function UptimeBar({
className,
days,
uptime,
label,
labelText,
showLegend = true,
size = "default",
timeZone = "UTC",
...props
}: UptimeBarProps) {
const percent = uptime ?? computeUptime(days)
const reading = `${uptimeText(percent)} uptime`
const first = days[0]
const last = days[days.length - 1]
// Inside the strip the record is carried by colour alone, so the whole run is
// summarised on the group and every day names itself on its own bar. The
// tooltip only ever reaches a pointer.
const summary = [
labelText ?? (typeof label === "string" ? label : "Daily status"),
`${days.length} ${days.length === 1 ? "day" : "days"}`,
reading,
].join(", ")
return (
<div
data-slot="uptime-bar"
data-size={size}
className={cn("flex w-full flex-col gap-2", className)}
{...props}
>
<div data-slot="uptime-bar-header" className="flex items-baseline justify-between gap-3">
{label ? (
<span
data-slot="uptime-bar-label"
className={cn(
"min-w-0 truncate text-muted-foreground",
size === "sm" ? "text-xs" : "text-sm"
)}
>
{label}
</span>
) : (
<span />
)}
<span
data-slot="uptime-bar-uptime"
className={cn(
"shrink-0 font-medium tabular-nums",
size === "sm" ? "text-xs" : "text-sm"
)}
>
{reading}
</span>
</div>
<div
data-slot="uptime-bar-track"
role="group"
aria-label={summary}
className="flex w-full items-stretch gap-px"
>
{days.map((day, index) => {
const date = formatDate(day.date, "medium", { timeZone })
const detail = day.label
? `${DAY_LABELS[day.status]}. ${day.label}`
: DAY_LABELS[day.status]
return (
<Tooltip key={index}>
<TooltipTrigger
render={
<span
data-slot="uptime-bar-day"
data-status={day.status}
role="img"
aria-label={`${date}: ${detail}`}
className={cn(uptimeBarDayVariants({ status: day.status, size }))}
/>
}
/>
{/* COUPLED TO registry/vibra/ui/tooltip.tsx: the popup is a
centred row by default, and these three cancel that so the
date can sit above its status. If a future upstream refresh
changes how TooltipContent lays its children out, revisit
these rather than adding more on top. */}
<TooltipContent className="flex-col items-start gap-0.5">
<span className="font-medium">{date}</span>
<span>{detail}</span>
</TooltipContent>
</Tooltip>
)
})}
</div>
{showLegend && first && last ? (
<div
data-slot="uptime-bar-legend"
className="flex items-baseline justify-between gap-3 text-xs text-muted-foreground"
>
<span>{formatDate(first.date, "medium", { timeZone })}</span>
<span>{formatDate(last.date, "medium", { timeZone })}</span>
</div>
) : null}
</div>
)
}
export { UptimeBar, uptimeBarDayVariants }