System status
A status page in one panel: a headline pill over one hairline-divided row per service.
No hooks of its own, so it renders on the server; the last-updated stamp is a RelativeTime, which is a client component that reads the clock safely on both sides. overallStatus is exported and pure: outage beats degraded beats maintenance beats operational, and an empty list is operational. The headline pill is a status region carrying a shaped icon, and every row names its status as text beside the dot, so nothing reads by colour alone. Colour is spent on the rows a reader has to act on — a healthy row keeps its green dot and an otherwise quiet reading. The HTML title attribute is replaced by the title prop.
Install
npx shadcn@latest add @vibra/system-statusNeeds the @vibra registry in your components.json — set it up once.
Examples
import { SystemStatus, type SystemStatusService } from "@/components/ui/system-status"
// Fixed rather than read from the clock, so the header renders the same string
// on the server and in the browser; RelativeTime does the counting from here.
const LAST_UPDATED = new Date("2026-09-04T09:41:00.000Z")
const SERVICES: SystemStatusService[] = [
{
name: "API",
status: "operational",
description: "api.vibra.example — 118 ms p95",
href: "#api",
},
{ name: "Web app", status: "operational", description: "app.vibra.example" },
{
name: "Background workers",
status: "degraded",
description: "Ingest queue is 12 minutes behind",
href: "#workers",
},
{ name: "Database", status: "operational", description: "Primary and two read replicas" },
]
export default function SystemStatusDemo() {
return (
<SystemStatus
className="w-full max-w-lg"
title="Vibra platform"
lastUpdated={LAST_UPDATED}
services={SERVICES}
/>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| services | { name: string; status: ServiceStatus; description?: string; href?: string }[] | — | One row each; the worst status among them sets the headline. |
| services[].name | string | — | Names the service, and keys the row; keep it unique across the list. |
| services[].status | "operational" | "degraded" | "outage" | "maintenance" | — | Sets the row's dot, its status word, and how much it weighs on the headline. |
| services[].description | string | — | A second line under the name — the host, the region, what is slow. |
| services[].href | string | — | Turns the row into a link to that service's own history. |
| lastUpdated | Date | string | — | When the page last reported in, rendered as a live relative time. |
| title | React.ReactNode | — | Sits above the pill, opposite the last-updated stamp. |
| overallStatus | (services: { status: ServiceStatus }[]) => ServiceStatus | — | The worst status in the list; exported for a headline you render yourself. |
Dependencies
Source
import * as React from "react"
import { CircleCheckIcon, OctagonAlertIcon, TriangleAlertIcon, WrenchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { RelativeTime } from "@/components/ui/relative-time"
import { StatusIndicator, type StatusIndicatorStatus } from "@/components/ui/status-indicator"
/** How one service is doing on a status page. */
export type ServiceStatus = "operational" | "degraded" | "outage" | "maintenance"
// Worst first. overallStatus walks this list and stops at the first status any
// service reports, so one outage outranks every degradation under it.
const SEVERITY_ORDER: ServiceStatus[] = ["outage", "degraded", "maintenance", "operational"]
/** The worst status any service reports: outage over degraded over maintenance over operational, and operational for an empty list. */
export function overallStatus(services: { status: ServiceStatus }[]): ServiceStatus {
return (
SEVERITY_ORDER.find((status) => services.some((service) => service.status === status)) ??
"operational"
)
}
// A sentence about the whole page, not the label of one row: a single degraded
// service is a partial outage to the person reading, whatever the row says.
const OVERALL_HEADLINES: Record<ServiceStatus, string> = {
operational: "All systems operational",
degraded: "Partial outage",
outage: "Major outage",
maintenance: "Under maintenance",
}
// Shape carries the state as well as colour does: a wrench never means the same as a check.
const OVERALL_ICONS: Record<ServiceStatus, React.ComponentType<React.ComponentProps<"svg">>> = {
operational: CircleCheckIcon,
degraded: TriangleAlertIcon,
outage: OctagonAlertIcon,
maintenance: WrenchIcon,
}
const OVERALL_TONES: Record<ServiceStatus, string> = {
operational: "bg-success-muted text-success",
degraded: "bg-warning-muted text-warning",
outage: "bg-danger-muted text-danger",
maintenance: "bg-info-muted text-info",
}
const SERVICE_LABELS: Record<ServiceStatus, string> = {
operational: "Operational",
degraded: "Degraded",
outage: "Outage",
maintenance: "Maintenance",
}
// The dot already speaks in health words; the service vocabulary maps onto them.
const SERVICE_DOTS: Record<ServiceStatus, StatusIndicatorStatus> = {
operational: "success",
degraded: "warning",
outage: "danger",
maintenance: "info",
}
// Colour is spent on the rows a reader has to act on. A healthy row keeps its
// green dot and an otherwise quiet reading, so a good page is not a wall of green.
const SERVICE_TONES: Record<ServiceStatus, string> = {
operational: "text-muted-foreground",
degraded: "text-warning",
outage: "text-danger",
maintenance: "text-info",
}
export type SystemStatusService = {
name: string
status: ServiceStatus
/** A second line under the name — the host, the region, what is slow. */
description?: string
/** Turns the row into a link to that service's own history. */
href?: string
}
// `title` is content here, not the HTML tooltip attribute, so it replaces it.
export type SystemStatusProps = Omit<React.ComponentProps<"div">, "title"> & {
services: SystemStatusService[]
/** When the page last reported in; rendered as a live relative time. */
lastUpdated?: Date | string
title?: React.ReactNode
}
function SystemStatus({
className,
services,
lastUpdated,
title,
...props
}: SystemStatusProps) {
const overall = overallStatus(services)
const OverallIcon = OVERALL_ICONS[overall]
return (
<div
data-slot="system-status"
data-status={overall}
className={cn("flex w-full flex-col gap-3", className)}
{...props}
>
{title || lastUpdated ? (
<div
data-slot="system-status-header"
className="flex items-baseline justify-between gap-3"
>
{title ? (
<span data-slot="system-status-title" className="text-sm font-medium">
{title}
</span>
) : (
<span />
)}
{lastUpdated ? (
<span data-slot="system-status-updated" className="text-xs text-muted-foreground">
{"Updated "}
<RelativeTime date={lastUpdated} />
</span>
) : null}
</div>
) : null}
<div
data-slot="system-status-pill"
data-status={overall}
// The one line a reader came for, and the only place colour is loud.
role="status"
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
OVERALL_TONES[overall]
)}
>
<OverallIcon aria-hidden="true" />
{OVERALL_HEADLINES[overall]}
</div>
{services.length > 0 ? (
<div
data-slot="system-status-services"
className="flex flex-col divide-y rounded-lg border"
>
{services.map((service) => {
const rowClassName = cn(
"flex items-center gap-3 px-3 py-2.5",
// No overflow-hidden on the panel, so the focus ring is never
// clipped; the end rows round their own fill instead.
service.href &&
"transition-colors first:rounded-t-lg last:rounded-b-lg hover:bg-muted/50 focus-ring"
)
const row = (
<>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span data-slot="system-status-name" className="truncate text-sm font-medium">
{service.name}
</span>
{service.description ? (
<span
data-slot="system-status-description"
className="truncate text-xs text-muted-foreground"
>
{service.description}
</span>
) : null}
</span>
<StatusIndicator
status={SERVICE_DOTS[service.status]}
label={SERVICE_LABELS[service.status]}
size="sm"
className={cn("shrink-0 font-medium", SERVICE_TONES[service.status])}
/>
</>
)
// Two tags rather than a computed one, so the href stays typed as an
// anchor's own prop.
return service.href ? (
<a
key={service.name}
data-slot="system-status-service"
data-status={service.status}
href={service.href}
className={rowClassName}
>
{row}
</a>
) : (
<div
key={service.name}
data-slot="system-status-service"
data-status={service.status}
className={rowClassName}
>
{row}
</div>
)
})}
</div>
) : null}
</div>
)
}
export { SystemStatus }