Skip to contentVibraUI
Feedback & status

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-status

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
services{ name: string; status: ServiceStatus; description?: string; href?: string }[]—One row each; the worst status among them sets the headline.
services[].namestring—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[].descriptionstring—A second line under the name — the host, the region, what is slow.
services[].hrefstring—Turns the row into a link to that service's own history.
lastUpdatedDate | string—When the page last reported in, rendered as a live relative time.
titleReact.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

components/ui/system-status.tsx
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 }