Status 1: system status page
A public status page: an overall banner worked out from the data, each service with a 90-day history you scrub by pointer or arrow keys, live incidents as a timeline of updates, two weeks of past incidents and a subscribe box.
The normal view. Pick a state to see what the block shows while it loads, is empty or fails.
// Ballmac UI: Status 1. https://ui.ballmac.com/blocks/status-1
"use client"
import * as React from "react"
import { CircleAlert, CircleCheck, CircleDashed, CircleX, Loader2, Mail, Wrench } from "lucide-react"
import { Button } from "@/components/ballmac/button"
import { Input } from "@/components/ballmac/input"
import { Skeleton } from "@/components/ballmac/skeleton"
import { frameProps, StateView, type StateProps } from "@/components/ballmac/state-view"
import { cn } from "@/lib/utils"
type Status1Level = "operational" | "degraded" | "outage" | "maintenance"
type Status1Day = "ok" | "degraded" | "outage" | "none"
type Status1Component = {
id: string
name: string
description?: string
/** Right now. */
status: Status1Level
/** One entry per day, oldest first, ending today. Ninety is the usual window. */
days: Status1Day[]
}
type Status1Update = { status: "investigating" | "identified" | "monitoring" | "resolved" | "scheduled"; /** "09:42" */ time: string; body: string }
type Status1Incident = {
id: string
title: string
impact: "minor" | "major" | "maintenance"
/** ISO date the incident started. */
day: string
/** Names of the affected components. */
affects?: string[]
/** Newest first. The first one's status is the incident's status. */
updates: Status1Update[]
}
type Status1Props = Omit<React.ComponentProps<"section">, "title"> &
StateProps & {
title?: string
components?: Status1Component[]
incidents?: Status1Incident[]
/** An ISO date for "today". The block never reads the clock. */
today?: string
/** "10:42 UTC", already written for people. */
updatedAt?: string
locale?: string
/** Called with the email to subscribe. Return a promise; reject to show its message. */
onSubscribe?: (email: string) => void | Promise<void>
}
const DAY = 86_400_000
const toT = (iso: string) => Date.parse(`${iso}T00:00:00Z`)
// A believable 90 days: integer maths only, so the server and the browser draw the same bars.
function history(seed: number, bad: Record<number, Status1Day> = {}): Status1Day[] {
return Array.from({ length: 90 }, (_, i) => bad[89 - i] ?? (((i * 37 + seed * 11) % 97 === 0 && i < 80) ? "degraded" : "ok"))
}
const sampleComponents: Status1Component[] = [
{ id: "api", name: "API", description: "Requests to api.acme.com", status: "degraded", days: history(1, { 0: "degraded", 17: "outage", 52: "degraded" }) },
{ id: "dash", name: "Dashboard", description: "The web app", status: "operational", days: history(2, { 17: "degraded" }) },
{ id: "hooks", name: "Webhooks", description: "Event delivery", status: "operational", days: history(3, { 33: "degraded", 34: "degraded" }) },
{ id: "auth", name: "Sign in", description: "Login, SSO and two-step codes", status: "operational", days: history(4) },
{ id: "files", name: "File storage", description: "Uploads and downloads", status: "maintenance", days: history(5, { 0: "none", 61: "degraded" }) },
{ id: "mail", name: "Email delivery", description: "Notifications and receipts", status: "operational", days: history(6, { 8: "outage", 9: "degraded" }) },
]
const sampleIncidents: Status1Incident[] = [
{
id: "i3",
title: "Slower responses from the API",
impact: "minor",
day: "2026-10-18",
affects: ["API"],
updates: [
{ status: "monitoring", time: "10:12", body: "A fix is live and response times are coming back down. We are watching it before we call it resolved." },
{ status: "identified", time: "09:40", body: "A busy database node is slowing some requests. We are moving its traffic to a healthy one." },
{ status: "investigating", time: "09:18", body: "Some requests are taking longer than usual. We are looking into it." },
],
},
{
id: "i2",
title: "File storage maintenance",
impact: "maintenance",
day: "2026-10-18",
affects: ["File storage"],
updates: [{ status: "scheduled", time: "08:00", body: "Uploads may pause for a few minutes while we move to faster disks. Downloads keep working." }],
},
{
id: "i0",
title: "Emails sent late",
impact: "minor",
day: "2026-10-14",
affects: ["Email delivery"],
updates: [
{ status: "resolved", time: "11:20", body: "The delay is over and every queued email has been sent." },
{ status: "investigating", time: "10:05", body: "Receipts and notifications are arriving up to an hour late." },
],
},
{
id: "i1",
title: "Webhook deliveries delayed",
impact: "major",
day: "2026-10-09",
affects: ["Webhooks", "API"],
updates: [
{ status: "resolved", time: "14:55", body: "Deliveries are caught up. Every event was delivered; none were lost." },
{ status: "identified", time: "14:05", body: "A queue stalled after a deploy. We rolled the deploy back and are replaying the backlog." },
{ status: "investigating", time: "13:42", body: "Webhooks are arriving late. We are investigating." },
],
},
]
const levelMeta: Record<Status1Level, { label: string; icon: typeof CircleCheck; tone: string }> = {
operational: { label: "Operational", icon: CircleCheck, tone: "text-chart-2" },
degraded: { label: "Degraded", icon: CircleAlert, tone: "text-amber-600 dark:text-amber-400" },
outage: { label: "Outage", icon: CircleX, tone: "text-destructive" },
maintenance: { label: "Maintenance", icon: Wrench, tone: "text-chart-1" },
}
const dayMeta: Record<Status1Day, { label: string; bar: string }> = {
ok: { label: "No issues", bar: "bg-chart-2" },
degraded: { label: "Degraded", bar: "bg-amber-400 dark:bg-amber-500" },
outage: { label: "Outage", bar: "bg-destructive" },
none: { label: "No data", bar: "bg-muted-foreground/25" },
}
const stepLabel: Record<Status1Update["status"], string> = { investigating: "Investigating", identified: "Identified", monitoring: "Monitoring", resolved: "Resolved", scheduled: "Scheduled" }
const impactLabel = { minor: "Minor", major: "Major", maintenance: "Maintenance" }
function Skel() {
return (
<div aria-hidden="true" className="grid gap-6">
<Skeleton className="h-24 rounded-2xl" />
<div className="rounded-2xl border p-6">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="py-4 first:pt-0 last:pb-0">
<Skeleton className="h-4 w-32" />
<Skeleton className="mt-3 h-8 w-full" />
</div>
))}
</div>
</div>
)
}
function UptimeBar({ item, today, locale }: { item: Status1Component; today: string; locale: string }) {
const fmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }), [locale])
const [at, setAt] = React.useState<number | null>(null)
const ref = React.useRef<HTMLDivElement>(null)
const n = item.days.length
const dateOf = (i: number) => fmt.format(toT(today) - (n - 1 - i) * DAY)
const counts = { outage: item.days.filter((d) => d === "outage").length, degraded: item.days.filter((d) => d === "degraded").length }
const known = item.days.filter((d) => d !== "none").length
// A day with an outage counts as down; a degraded day counts as half.
const uptime = known ? (100 * (known - counts.outage - counts.degraded * 0.5)) / known : 100
const summary = `${uptime.toFixed(2)}% uptime over ${n} days: ${counts.outage} ${counts.outage === 1 ? "day" : "days"} with an outage, ${counts.degraded} degraded`
const move = (e: React.KeyboardEvent) => {
const cur = at ?? n - 1
const next = e.key === "ArrowLeft" ? cur - 1 : e.key === "ArrowRight" ? cur + 1 : e.key === "Home" ? 0 : e.key === "End" ? n - 1 : null
if (next === null) return
e.preventDefault()
setAt(Math.max(0, Math.min(n - 1, next)))
}
return (
<div>
<div
ref={ref}
dir="ltr"
role="slider"
tabIndex={0}
aria-label={`${item.name} history, one bar per day`}
aria-valuemin={0}
aria-valuemax={n - 1}
aria-valuenow={at ?? n - 1}
aria-valuetext={at === null ? summary : `${dateOf(at)}: ${dayMeta[item.days[at]!].label}`}
onKeyDown={move}
onFocus={() => setAt((a) => a ?? n - 1)}
onBlur={() => setAt(null)}
onPointerLeave={() => setAt(null)}
onPointerMove={(e) => {
const r = ref.current!.getBoundingClientRect()
setAt(Math.max(0, Math.min(n - 1, Math.floor(((e.clientX - r.left) / r.width) * n))))
}}
className="focus-visible:ring-ring/50 relative flex h-9 cursor-crosshair items-stretch gap-[2px] rounded-md outline-none focus-visible:ring-[3px] sm:gap-[3px]"
>
{item.days.map((d, i) => (
<span key={i} aria-hidden="true" className={cn("min-w-0 flex-1 rounded-[2px] transition-[opacity,transform] duration-100 motion-reduce:transition-none", dayMeta[d].bar, at !== null && at !== i && "opacity-60", at === i && "scale-y-110")} />
))}
{at !== null ? (
<span
aria-hidden="true"
className="bg-popover text-popover-foreground pointer-events-none absolute -top-11 z-10 -translate-x-1/2 rounded-md border px-2.5 py-1.5 text-xs whitespace-nowrap shadow-md"
style={{ left: `clamp(4.5rem, ${((at + 0.5) / n) * 100}%, calc(100% - 4.5rem))` }}
>
<span className="font-medium">{dateOf(at)}</span> <span className="text-muted-foreground">· {dayMeta[item.days[at]!].label}</span>
</span>
) : null}
</div>
<div aria-hidden="true" dir="ltr" className="text-muted-foreground mt-1.5 flex justify-between text-xs">
<span>{n} days ago</span>
<span>{uptime.toFixed(2)}% uptime</span>
<span>Today</span>
</div>
<p className="sr-only">{summary}</p>
</div>
)
}
/**
* A public status page: one banner that says how things are overall, each service with a 90-day history you can scrub with a pointer or
* the arrow keys, what is happening now as a timeline of updates, the last two weeks of incidents, and a subscribe box. The overall
* message and every uptime figure are worked out from the data you pass.
*/
function Status1({
title = "Acme status",
components = sampleComponents,
incidents = sampleIncidents,
today = "2026-10-18",
updatedAt = "10:42 UTC",
locale = "en",
onSubscribe,
state = "ready",
onRetry,
emptyState,
errorState,
className,
...props
}: Status1Props) {
const id = React.useId()
const list = state === "empty" ? [] : components
const active = incidents.filter((i) => i.updates[0]?.status !== "resolved")
const level: Status1Level = list.some((c) => c.status === "outage") ? "outage" : list.some((c) => c.status === "degraded") ? "degraded" : list.some((c) => c.status === "maintenance") ? "maintenance" : "operational"
const overall = {
operational: { text: "All systems operational", sub: "Everything is running normally.", box: "bg-chart-2/10 border-chart-2/30" },
degraded: { text: "Some systems are slower than usual", sub: "We know and we are working on it.", box: "bg-amber-500/10 border-amber-500/40" },
outage: { text: "Some systems are down", sub: "We are working on it right now.", box: "bg-destructive/10 border-destructive/30" },
maintenance: { text: "Scheduled maintenance in progress", sub: "Some things may pause for a short while.", box: "bg-chart-1/10 border-chart-1/30" },
}[level]
const L = levelMeta[level]
const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", timeZone: "UTC" }), [locale])
const [email, setEmail] = React.useState("")
const [sub, setSub] = React.useState<"idle" | "sending" | "done">("idle")
const [subError, setSubError] = React.useState<string>()
async function subscribe(e: React.FormEvent) {
e.preventDefault()
if (!/^\S+@\S+\.\S+$/.test(email.trim())) return setSubError("Enter an email address like name@company.com.")
setSubError(undefined)
setSub("sending")
try {
await onSubscribe?.(email.trim())
setSub("done")
} catch (err) {
setSub("idle")
setSubError(err instanceof Error ? err.message : "We could not subscribe you. Try again.")
}
}
// The last 14 days, newest first, each with its incidents (or the plain statement that there were none).
const days = Array.from({ length: 14 }, (_, i) => new Date(toT(today) - i * DAY).toISOString().slice(0, 10))
const past = incidents.filter((i) => i.updates[0]?.status === "resolved")
// Quiet days that follow each other share one row instead of a wall of "nothing happened".
const rows: { days: string[]; here: Status1Incident[] }[] = []
for (const d of days) {
const here = past.filter((i) => i.day === d)
const last = rows.at(-1)
if (!here.length && last && !last.here.length) last.days.push(d)
else rows.push({ days: [d], here })
}
const content = !list.length ? (
<div className="bg-card rounded-2xl border px-6 py-16 text-center">
<span aria-hidden="true" className="bg-muted mx-auto flex size-12 items-center justify-center rounded-full">
<CircleDashed className="size-6" />
</span>
<h2 className="mt-5 text-lg font-semibold tracking-tight">{emptyState?.title ?? "No services are listed yet"}</h2>
<p className="text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty">{emptyState?.description ?? "Once services are added, their status and history appear here."}</p>
{emptyState?.action}
</div>
) : (
<div className="grid gap-10">
<div role="status" className={cn("flex items-center gap-4 rounded-2xl border p-5 sm:p-6", overall.box)}>
<L.icon aria-hidden="true" className={cn("size-8 shrink-0", L.tone)} />
<div className="min-w-0">
<p className="text-xl font-semibold tracking-tight text-balance sm:text-2xl">{overall.text}</p>
<p className="text-muted-foreground mt-0.5 text-sm">
{overall.sub} Updated {updatedAt}.
</p>
</div>
</div>
{active.length ? (
<section aria-labelledby={`${id}-now`}>
<h2 id={`${id}-now`} className="mb-4 text-lg font-semibold tracking-tight">
Happening now
</h2>
<ul className="grid gap-4">
{active.map((inc) => (
<li key={inc.id} className="bg-card rounded-2xl border p-5 sm:p-6">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<h3 className="font-semibold tracking-tight">{inc.title}</h3>
<span className="bg-muted rounded-full border px-2.5 py-0.5 text-xs font-medium">{impactLabel[inc.impact]}</span>
{inc.affects?.length ? <span className="text-muted-foreground text-sm">Affects {inc.affects.join(", ")}</span> : null}
</div>
<ol className="mt-5 grid gap-5">
{inc.updates.map((u, i) => (
<li key={i} className="relative ps-7">
<span aria-hidden="true" className={cn("absolute start-0 top-1.5 size-3 rounded-full border-2", i === 0 ? "bg-foreground border-foreground" : "bg-background border-border")} />
{i < inc.updates.length - 1 ? <span aria-hidden="true" className="bg-border absolute start-[5px] top-5 -bottom-6 w-px" /> : null}
<p className="text-sm">
<span className="font-semibold">{stepLabel[u.status]}</span> <span className="text-muted-foreground tabular-nums">· {u.time}</span>
</p>
<p className="text-muted-foreground mt-1 text-sm text-pretty">{u.body}</p>
</li>
))}
</ol>
</li>
))}
</ul>
</section>
) : null}
<section aria-labelledby={`${id}-svc`}>
<h2 id={`${id}-svc`} className="mb-4 text-lg font-semibold tracking-tight">
Services
</h2>
<ul className="bg-card divide-y rounded-2xl border">
{list.map((c) => {
const M = levelMeta[c.status]
return (
<li key={c.id} className="p-5 sm:p-6">
<div className="mb-4 flex items-start justify-between gap-4">
<div className="min-w-0">
<h3 className="font-medium">{c.name}</h3>
{c.description ? <p className="text-muted-foreground mt-0.5 text-sm">{c.description}</p> : null}
</div>
<p className="flex shrink-0 items-center gap-1.5 text-sm font-medium">
<M.icon aria-hidden="true" className={cn("size-4", M.tone)} />
{M.label}
</p>
</div>
<UptimeBar item={c} today={today} locale={locale} />
</li>
)
})}
</ul>
</section>
<section aria-labelledby={`${id}-past`}>
<h2 id={`${id}-past`} className="mb-4 text-lg font-semibold tracking-tight">
Past incidents
</h2>
<ol className="grid gap-6">
{rows.map((r) => (
<li key={r.days[0]} className="grid gap-3 sm:grid-cols-[9rem_minmax(0,1fr)]">
<h3 className="text-muted-foreground text-sm font-medium tabular-nums">{r.days.length > 1 ? `${dayFmt.format(toT(r.days.at(-1)!))} to ${dayFmt.format(toT(r.days[0]!))}` : dayFmt.format(toT(r.days[0]!))}</h3>
{r.here.length ? (
<ul className="grid gap-3">
{r.here.map((inc) => (
<li key={inc.id} className="bg-card rounded-xl border p-4">
<p className="font-medium">{inc.title}</p>
<p className="text-muted-foreground mt-1 text-sm text-pretty">{inc.updates[0]?.body}</p>
<p className="text-muted-foreground mt-2 text-xs">
{impactLabel[inc.impact]}
{inc.affects?.length ? ` · ${inc.affects.join(", ")}` : ""} · Resolved {inc.updates[0]?.time}
</p>
</li>
))}
</ul>
) : (
<p className="text-muted-foreground text-sm">No incidents reported.</p>
)}
</li>
))}
</ol>
</section>
<section aria-labelledby={`${id}-sub`} className="bg-muted/40 rounded-2xl border p-5 sm:p-6">
<h2 id={`${id}-sub`} className="flex items-center gap-2 text-lg font-semibold tracking-tight">
<Mail aria-hidden="true" className="size-5" />
Get told when something changes
</h2>
{sub === "done" ? (
<p role="status" className="mt-3 text-sm">
Subscribed. We will email {email.trim()} when an incident starts or ends.
</p>
) : (
<form onSubmit={subscribe} noValidate className="mt-3 flex flex-col gap-2 sm:flex-row">
<div className="flex-1">
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} aria-label="Email address" placeholder="name@company.com" autoComplete="email" aria-invalid={!!subError} aria-describedby={subError ? `${id}-err` : undefined} />
{subError ? (
<p id={`${id}-err`} role="alert" className="text-destructive mt-1.5 text-sm">
{subError}
</p>
) : null}
</div>
<Button type="submit" disabled={sub === "sending"} className="sm:self-start">
{sub === "sending" ? <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" /> : null}
{sub === "sending" ? "Subscribing" : "Subscribe"}
</Button>
</form>
)}
</section>
</div>
)
return (
<section data-slot="status-1" data-state={state} aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-3xl px-4 py-12 sm:px-6 sm:py-16", className)} {...frameProps(props)}>
<h1 id={`${id}-title`} className="mb-8 text-3xl font-semibold tracking-[-0.03em] text-balance">
{title}
</h1>
<StateView state={state === "loading" || state === "error" ? state : "ready"} onRetry={onRetry} label="Loading system status" loading={<Skel />} error={{ title: errorState?.title ?? "Status could not be loaded", description: errorState?.description ?? "This does not mean something is down. Check your connection and try again.", ...errorState }}>
{content}
</StateView>
</section>
)
}
export { Status1, type Status1Props, type Status1Component, type Status1Incident, type Status1Update, type Status1Level, type Status1Day }Installation
$ pnpm dlx shadcn@latest add @ballmac/status-1Install the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/button @ballmac/input @ballmac/skeleton @ballmac/state-viewCopy the source into your project.
components/ballmac/blocks/status-1/status-1.tsx// Ballmac UI: Status 1. https://ui.ballmac.com/blocks/status-1 "use client" import * as React from "react" import { CircleAlert, CircleCheck, CircleDashed, CircleX, Loader2, Mail, Wrench } from "lucide-react" import { Button } from "@/components/ballmac/button" import { Input } from "@/components/ballmac/input" import { Skeleton } from "@/components/ballmac/skeleton" import { frameProps, StateView, type StateProps } from "@/components/ballmac/state-view" import { cn } from "@/lib/utils" type Status1Level = "operational" | "degraded" | "outage" | "maintenance" type Status1Day = "ok" | "degraded" | "outage" | "none" type Status1Component = { id: string name: string description?: string /** Right now. */ status: Status1Level /** One entry per day, oldest first, ending today. Ninety is the usual window. */ days: Status1Day[] } type Status1Update = { status: "investigating" | "identified" | "monitoring" | "resolved" | "scheduled"; /** "09:42" */ time: string; body: string } type Status1Incident = { id: string title: string impact: "minor" | "major" | "maintenance" /** ISO date the incident started. */ day: string /** Names of the affected components. */ affects?: string[] /** Newest first. The first one's status is the incident's status. */ updates: Status1Update[] } type Status1Props = Omit<React.ComponentProps<"section">, "title"> & StateProps & { title?: string components?: Status1Component[] incidents?: Status1Incident[] /** An ISO date for "today". The block never reads the clock. */ today?: string /** "10:42 UTC", already written for people. */ updatedAt?: string locale?: string /** Called with the email to subscribe. Return a promise; reject to show its message. */ onSubscribe?: (email: string) => void | Promise<void> } const DAY = 86_400_000 const toT = (iso: string) => Date.parse(`${iso}T00:00:00Z`) // A believable 90 days: integer maths only, so the server and the browser draw the same bars. function history(seed: number, bad: Record<number, Status1Day> = {}): Status1Day[] { return Array.from({ length: 90 }, (_, i) => bad[89 - i] ?? (((i * 37 + seed * 11) % 97 === 0 && i < 80) ? "degraded" : "ok")) } const sampleComponents: Status1Component[] = [ { id: "api", name: "API", description: "Requests to api.acme.com", status: "degraded", days: history(1, { 0: "degraded", 17: "outage", 52: "degraded" }) }, { id: "dash", name: "Dashboard", description: "The web app", status: "operational", days: history(2, { 17: "degraded" }) }, { id: "hooks", name: "Webhooks", description: "Event delivery", status: "operational", days: history(3, { 33: "degraded", 34: "degraded" }) }, { id: "auth", name: "Sign in", description: "Login, SSO and two-step codes", status: "operational", days: history(4) }, { id: "files", name: "File storage", description: "Uploads and downloads", status: "maintenance", days: history(5, { 0: "none", 61: "degraded" }) }, { id: "mail", name: "Email delivery", description: "Notifications and receipts", status: "operational", days: history(6, { 8: "outage", 9: "degraded" }) }, ] const sampleIncidents: Status1Incident[] = [ { id: "i3", title: "Slower responses from the API", impact: "minor", day: "2026-10-18", affects: ["API"], updates: [ { status: "monitoring", time: "10:12", body: "A fix is live and response times are coming back down. We are watching it before we call it resolved." }, { status: "identified", time: "09:40", body: "A busy database node is slowing some requests. We are moving its traffic to a healthy one." }, { status: "investigating", time: "09:18", body: "Some requests are taking longer than usual. We are looking into it." }, ], }, { id: "i2", title: "File storage maintenance", impact: "maintenance", day: "2026-10-18", affects: ["File storage"], updates: [{ status: "scheduled", time: "08:00", body: "Uploads may pause for a few minutes while we move to faster disks. Downloads keep working." }], }, { id: "i0", title: "Emails sent late", impact: "minor", day: "2026-10-14", affects: ["Email delivery"], updates: [ { status: "resolved", time: "11:20", body: "The delay is over and every queued email has been sent." }, { status: "investigating", time: "10:05", body: "Receipts and notifications are arriving up to an hour late." }, ], }, { id: "i1", title: "Webhook deliveries delayed", impact: "major", day: "2026-10-09", affects: ["Webhooks", "API"], updates: [ { status: "resolved", time: "14:55", body: "Deliveries are caught up. Every event was delivered; none were lost." }, { status: "identified", time: "14:05", body: "A queue stalled after a deploy. We rolled the deploy back and are replaying the backlog." }, { status: "investigating", time: "13:42", body: "Webhooks are arriving late. We are investigating." }, ], }, ] const levelMeta: Record<Status1Level, { label: string; icon: typeof CircleCheck; tone: string }> = { operational: { label: "Operational", icon: CircleCheck, tone: "text-chart-2" }, degraded: { label: "Degraded", icon: CircleAlert, tone: "text-amber-600 dark:text-amber-400" }, outage: { label: "Outage", icon: CircleX, tone: "text-destructive" }, maintenance: { label: "Maintenance", icon: Wrench, tone: "text-chart-1" }, } const dayMeta: Record<Status1Day, { label: string; bar: string }> = { ok: { label: "No issues", bar: "bg-chart-2" }, degraded: { label: "Degraded", bar: "bg-amber-400 dark:bg-amber-500" }, outage: { label: "Outage", bar: "bg-destructive" }, none: { label: "No data", bar: "bg-muted-foreground/25" }, } const stepLabel: Record<Status1Update["status"], string> = { investigating: "Investigating", identified: "Identified", monitoring: "Monitoring", resolved: "Resolved", scheduled: "Scheduled" } const impactLabel = { minor: "Minor", major: "Major", maintenance: "Maintenance" } function Skel() { return ( <div aria-hidden="true" className="grid gap-6"> <Skeleton className="h-24 rounded-2xl" /> <div className="rounded-2xl border p-6"> {[0, 1, 2, 3].map((i) => ( <div key={i} className="py-4 first:pt-0 last:pb-0"> <Skeleton className="h-4 w-32" /> <Skeleton className="mt-3 h-8 w-full" /> </div> ))} </div> </div> ) } function UptimeBar({ item, today, locale }: { item: Status1Component; today: string; locale: string }) { const fmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }), [locale]) const [at, setAt] = React.useState<number | null>(null) const ref = React.useRef<HTMLDivElement>(null) const n = item.days.length const dateOf = (i: number) => fmt.format(toT(today) - (n - 1 - i) * DAY) const counts = { outage: item.days.filter((d) => d === "outage").length, degraded: item.days.filter((d) => d === "degraded").length } const known = item.days.filter((d) => d !== "none").length // A day with an outage counts as down; a degraded day counts as half. const uptime = known ? (100 * (known - counts.outage - counts.degraded * 0.5)) / known : 100 const summary = `${uptime.toFixed(2)}% uptime over ${n} days: ${counts.outage} ${counts.outage === 1 ? "day" : "days"} with an outage, ${counts.degraded} degraded` const move = (e: React.KeyboardEvent) => { const cur = at ?? n - 1 const next = e.key === "ArrowLeft" ? cur - 1 : e.key === "ArrowRight" ? cur + 1 : e.key === "Home" ? 0 : e.key === "End" ? n - 1 : null if (next === null) return e.preventDefault() setAt(Math.max(0, Math.min(n - 1, next))) } return ( <div> <div ref={ref} dir="ltr" role="slider" tabIndex={0} aria-label={`${item.name} history, one bar per day`} aria-valuemin={0} aria-valuemax={n - 1} aria-valuenow={at ?? n - 1} aria-valuetext={at === null ? summary : `${dateOf(at)}: ${dayMeta[item.days[at]!].label}`} onKeyDown={move} onFocus={() => setAt((a) => a ?? n - 1)} onBlur={() => setAt(null)} onPointerLeave={() => setAt(null)} onPointerMove={(e) => { const r = ref.current!.getBoundingClientRect() setAt(Math.max(0, Math.min(n - 1, Math.floor(((e.clientX - r.left) / r.width) * n)))) }} className="focus-visible:ring-ring/50 relative flex h-9 cursor-crosshair items-stretch gap-[2px] rounded-md outline-none focus-visible:ring-[3px] sm:gap-[3px]" > {item.days.map((d, i) => ( <span key={i} aria-hidden="true" className={cn("min-w-0 flex-1 rounded-[2px] transition-[opacity,transform] duration-100 motion-reduce:transition-none", dayMeta[d].bar, at !== null && at !== i && "opacity-60", at === i && "scale-y-110")} /> ))} {at !== null ? ( <span aria-hidden="true" className="bg-popover text-popover-foreground pointer-events-none absolute -top-11 z-10 -translate-x-1/2 rounded-md border px-2.5 py-1.5 text-xs whitespace-nowrap shadow-md" style={{ left: `clamp(4.5rem, ${((at + 0.5) / n) * 100}%, calc(100% - 4.5rem))` }} > <span className="font-medium">{dateOf(at)}</span> <span className="text-muted-foreground">· {dayMeta[item.days[at]!].label}</span> </span> ) : null} </div> <div aria-hidden="true" dir="ltr" className="text-muted-foreground mt-1.5 flex justify-between text-xs"> <span>{n} days ago</span> <span>{uptime.toFixed(2)}% uptime</span> <span>Today</span> </div> <p className="sr-only">{summary}</p> </div> ) } /** * A public status page: one banner that says how things are overall, each service with a 90-day history you can scrub with a pointer or * the arrow keys, what is happening now as a timeline of updates, the last two weeks of incidents, and a subscribe box. The overall * message and every uptime figure are worked out from the data you pass. */ function Status1({ title = "Acme status", components = sampleComponents, incidents = sampleIncidents, today = "2026-10-18", updatedAt = "10:42 UTC", locale = "en", onSubscribe, state = "ready", onRetry, emptyState, errorState, className, ...props }: Status1Props) { const id = React.useId() const list = state === "empty" ? [] : components const active = incidents.filter((i) => i.updates[0]?.status !== "resolved") const level: Status1Level = list.some((c) => c.status === "outage") ? "outage" : list.some((c) => c.status === "degraded") ? "degraded" : list.some((c) => c.status === "maintenance") ? "maintenance" : "operational" const overall = { operational: { text: "All systems operational", sub: "Everything is running normally.", box: "bg-chart-2/10 border-chart-2/30" }, degraded: { text: "Some systems are slower than usual", sub: "We know and we are working on it.", box: "bg-amber-500/10 border-amber-500/40" }, outage: { text: "Some systems are down", sub: "We are working on it right now.", box: "bg-destructive/10 border-destructive/30" }, maintenance: { text: "Scheduled maintenance in progress", sub: "Some things may pause for a short while.", box: "bg-chart-1/10 border-chart-1/30" }, }[level] const L = levelMeta[level] const dayFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", timeZone: "UTC" }), [locale]) const [email, setEmail] = React.useState("") const [sub, setSub] = React.useState<"idle" | "sending" | "done">("idle") const [subError, setSubError] = React.useState<string>() async function subscribe(e: React.FormEvent) { e.preventDefault() if (!/^\S+@\S+\.\S+$/.test(email.trim())) return setSubError("Enter an email address like name@company.com.") setSubError(undefined) setSub("sending") try { await onSubscribe?.(email.trim()) setSub("done") } catch (err) { setSub("idle") setSubError(err instanceof Error ? err.message : "We could not subscribe you. Try again.") } } // The last 14 days, newest first, each with its incidents (or the plain statement that there were none). const days = Array.from({ length: 14 }, (_, i) => new Date(toT(today) - i * DAY).toISOString().slice(0, 10)) const past = incidents.filter((i) => i.updates[0]?.status === "resolved") // Quiet days that follow each other share one row instead of a wall of "nothing happened". const rows: { days: string[]; here: Status1Incident[] }[] = [] for (const d of days) { const here = past.filter((i) => i.day === d) const last = rows.at(-1) if (!here.length && last && !last.here.length) last.days.push(d) else rows.push({ days: [d], here }) } const content = !list.length ? ( <div className="bg-card rounded-2xl border px-6 py-16 text-center"> <span aria-hidden="true" className="bg-muted mx-auto flex size-12 items-center justify-center rounded-full"> <CircleDashed className="size-6" /> </span> <h2 className="mt-5 text-lg font-semibold tracking-tight">{emptyState?.title ?? "No services are listed yet"}</h2> <p className="text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty">{emptyState?.description ?? "Once services are added, their status and history appear here."}</p> {emptyState?.action} </div> ) : ( <div className="grid gap-10"> <div role="status" className={cn("flex items-center gap-4 rounded-2xl border p-5 sm:p-6", overall.box)}> <L.icon aria-hidden="true" className={cn("size-8 shrink-0", L.tone)} /> <div className="min-w-0"> <p className="text-xl font-semibold tracking-tight text-balance sm:text-2xl">{overall.text}</p> <p className="text-muted-foreground mt-0.5 text-sm"> {overall.sub} Updated {updatedAt}. </p> </div> </div> {active.length ? ( <section aria-labelledby={`${id}-now`}> <h2 id={`${id}-now`} className="mb-4 text-lg font-semibold tracking-tight"> Happening now </h2> <ul className="grid gap-4"> {active.map((inc) => ( <li key={inc.id} className="bg-card rounded-2xl border p-5 sm:p-6"> <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5"> <h3 className="font-semibold tracking-tight">{inc.title}</h3> <span className="bg-muted rounded-full border px-2.5 py-0.5 text-xs font-medium">{impactLabel[inc.impact]}</span> {inc.affects?.length ? <span className="text-muted-foreground text-sm">Affects {inc.affects.join(", ")}</span> : null} </div> <ol className="mt-5 grid gap-5"> {inc.updates.map((u, i) => ( <li key={i} className="relative ps-7"> <span aria-hidden="true" className={cn("absolute start-0 top-1.5 size-3 rounded-full border-2", i === 0 ? "bg-foreground border-foreground" : "bg-background border-border")} /> {i < inc.updates.length - 1 ? <span aria-hidden="true" className="bg-border absolute start-[5px] top-5 -bottom-6 w-px" /> : null} <p className="text-sm"> <span className="font-semibold">{stepLabel[u.status]}</span> <span className="text-muted-foreground tabular-nums">· {u.time}</span> </p> <p className="text-muted-foreground mt-1 text-sm text-pretty">{u.body}</p> </li> ))} </ol> </li> ))} </ul> </section> ) : null} <section aria-labelledby={`${id}-svc`}> <h2 id={`${id}-svc`} className="mb-4 text-lg font-semibold tracking-tight"> Services </h2> <ul className="bg-card divide-y rounded-2xl border"> {list.map((c) => { const M = levelMeta[c.status] return ( <li key={c.id} className="p-5 sm:p-6"> <div className="mb-4 flex items-start justify-between gap-4"> <div className="min-w-0"> <h3 className="font-medium">{c.name}</h3> {c.description ? <p className="text-muted-foreground mt-0.5 text-sm">{c.description}</p> : null} </div> <p className="flex shrink-0 items-center gap-1.5 text-sm font-medium"> <M.icon aria-hidden="true" className={cn("size-4", M.tone)} /> {M.label} </p> </div> <UptimeBar item={c} today={today} locale={locale} /> </li> ) })} </ul> </section> <section aria-labelledby={`${id}-past`}> <h2 id={`${id}-past`} className="mb-4 text-lg font-semibold tracking-tight"> Past incidents </h2> <ol className="grid gap-6"> {rows.map((r) => ( <li key={r.days[0]} className="grid gap-3 sm:grid-cols-[9rem_minmax(0,1fr)]"> <h3 className="text-muted-foreground text-sm font-medium tabular-nums">{r.days.length > 1 ? `${dayFmt.format(toT(r.days.at(-1)!))} to ${dayFmt.format(toT(r.days[0]!))}` : dayFmt.format(toT(r.days[0]!))}</h3> {r.here.length ? ( <ul className="grid gap-3"> {r.here.map((inc) => ( <li key={inc.id} className="bg-card rounded-xl border p-4"> <p className="font-medium">{inc.title}</p> <p className="text-muted-foreground mt-1 text-sm text-pretty">{inc.updates[0]?.body}</p> <p className="text-muted-foreground mt-2 text-xs"> {impactLabel[inc.impact]} {inc.affects?.length ? ` · ${inc.affects.join(", ")}` : ""} · Resolved {inc.updates[0]?.time} </p> </li> ))} </ul> ) : ( <p className="text-muted-foreground text-sm">No incidents reported.</p> )} </li> ))} </ol> </section> <section aria-labelledby={`${id}-sub`} className="bg-muted/40 rounded-2xl border p-5 sm:p-6"> <h2 id={`${id}-sub`} className="flex items-center gap-2 text-lg font-semibold tracking-tight"> <Mail aria-hidden="true" className="size-5" /> Get told when something changes </h2> {sub === "done" ? ( <p role="status" className="mt-3 text-sm"> Subscribed. We will email {email.trim()} when an incident starts or ends. </p> ) : ( <form onSubmit={subscribe} noValidate className="mt-3 flex flex-col gap-2 sm:flex-row"> <div className="flex-1"> <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} aria-label="Email address" placeholder="name@company.com" autoComplete="email" aria-invalid={!!subError} aria-describedby={subError ? `${id}-err` : undefined} /> {subError ? ( <p id={`${id}-err`} role="alert" className="text-destructive mt-1.5 text-sm"> {subError} </p> ) : null} </div> <Button type="submit" disabled={sub === "sending"} className="sm:self-start"> {sub === "sending" ? <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" /> : null} {sub === "sending" ? "Subscribing" : "Subscribe"} </Button> </form> )} </section> </div> ) return ( <section data-slot="status-1" data-state={state} aria-labelledby={`${id}-title`} className={cn("mx-auto w-full max-w-3xl px-4 py-12 sm:px-6 sm:py-16", className)} {...frameProps(props)}> <h1 id={`${id}-title`} className="mb-8 text-3xl font-semibold tracking-[-0.03em] text-balance"> {title} </h1> <StateView state={state === "loading" || state === "error" ? state : "ready"} onRetry={onRetry} label="Loading system status" loading={<Skel />} error={{ title: errorState?.title ?? "Status could not be loaded", description: errorState?.description ?? "This does not mean something is down. Check your connection and try again.", ...errorState }}> {content} </StateView> </section> ) } export { Status1, type Status1Props, type Status1Component, type Status1Incident, type Status1Update, type Status1Level, type Status1Day }Update the import paths to match your project setup.
Usage
"use client"
import { Status1 } from "@/components/ballmac/blocks/status-1/status-1"
export default function Status1Demo() {
return <Status1 />
}States
Pass state and the block does the rest: skeletons that match its layout, a clear empty message, an error you can retry, and announcements for screen readers.
Loading
A skeleton banner and service rows under the real heading, announced as loading.
state="loading"No services listed
Says services appear once added; emptyState sets the copy and can add a button.
state="empty" (or components={[]})Could not load
An alert that says this does not mean something is down, with Try again (onRetry).
state="error"
Quality scorecard
Every applicable check passes
Measured in Chromium 153.0.8010.12 on October 19, 2026, on version 1.0.0, across 6 examples.
Passed: Accessibility scan
No serious or critical issues in light and dark, on a phone and a desktop (axe-core 4.13.0, WCAG 2.1 AA and best-practice rules).
Passed: Small screens
No sideways scrolling at 360, 390 or 1280 px wide.
Passed: Reduced motion
Every endless animation stops when the visitor asks for less motion.
Not applicable: Right to left
Not mirrored by design: The 90-day history bars run oldest to newest from left to right in every language, like a chart, and make up most of the page's elements.
Passed: Clean console
No console errors or React hydration errors while loading.
Passed: Keyboard
19 tab stops, each showing a visible change when it takes focus. 3 documented keys.
Passed: Loading, empty and error
Ships loading, empty, error states, each with an example.
Passed: Automated tests
Covered by status-1.test.tsx.
For information: Footprint
6.6 KB of source (gzip) in 1 file. Installs lucide-react.
Automated checks find a subset of problems and do not replace testing with a screen reader and a keyboard in your own app. What they miss.
Dependencies
- npm
- lucide-react
Use with AI
A status page. <Status1 components={[{ id, name, description, status: 'operational' | 'degraded' | 'outage' | 'maintenance', days: ('ok' | 'degraded' | 'outage' | 'none')[] /* 90, oldest first */ }]} incidents={[{ id, title, impact: 'minor' | 'major' | 'maintenance', day: '2026-10-18', affects: ['API'], updates: [{ status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'scheduled', time: '09:42', body }] /* newest first */ }]} today='2026-10-18' updatedAt='10:42 UTC' onSubscribe />. The banner is the worst current status; an incident is live until its newest update is resolved. Pass today yourself; the block never reads the clock. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Status 1: system status page (@ballmac/status-1) to this project with the shadcn MCP, then use it where it fits.
Use it for
- A public status page for a service
- An internal reliability page showing uptime and incidents
Not for
- A single status dot or badge row (use status-dot or status-badge-row)
- Raw server logs (use log-stream)
Registry JSON: https://ui.ballmac.com/r/status-1.json
Free to use in personal and commercial projects.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.