# 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.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/status-1).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/status-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/status-1-light-3a2ab7e03eb19dfd.png
- Registry item: https://ui.ballmac.com/r/status-1.json
- Categories: blocks, status
- Listed: 2026-10-18, source last changed 2026-10-18

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/ballmac/status-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://ui.ballmac.com/r/button.json`, `https://ui.ballmac.com/r/input.json`, `https://ui.ballmac.com/r/skeleton.json`, `https://ui.ballmac.com/r/state-view.json`

## Files

### `registry/ballmac/components/blocks/status-1/status-1.tsx`

```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 }
```
