A status page block: an overall banner and one row per service with a status chip, uptime and a 90-day bar history you can explore with the pointer or arrow keys.
All systems operational
Updated a minute ago
- 99.98% uptimeOperational
API
REST and GraphQL
- 99.99% uptimeOperational
Dashboard
app.example.com
- 99.94% uptimeOperational
Webhooks
Outgoing deliveries
- 100.00% uptimeOperational
Background jobs
import { StatusBadgeRow, type ServiceDay } from "@/components/ballmac/status-badge-row"
function history(seed: number, incidents: Record<number, ServiceDay> = {}): ServiceDay[] {
return Array.from({ length: 60 }, (_, i) => incidents[i] ?? "operational")
}
export default function StatusBadgeRowDemo() {
return (
<div className="w-full max-w-xl">
<StatusBadgeRow
endDate="2026-09-30"
updated="Updated a minute ago"
services={[
{ name: "API", description: "REST and GraphQL", status: "operational", uptime: 99.98, days: history(1, { 14: { status: "degraded", note: "Elevated latency" }, 41: "maintenance" }) },
{ name: "Dashboard", description: "app.example.com", status: "operational", uptime: 99.99, days: history(2, { 28: { status: "degraded", note: "Slow page loads" } }) },
{ name: "Webhooks", description: "Outgoing deliveries", status: "operational", uptime: 99.94, days: history(3, { 9: { status: "outage", note: "Queue backlog" }, 10: "degraded" }) },
{ name: "Background jobs", status: "operational", uptime: 100, days: history(4) },
]}
/>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/status-badge-rowInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18n @ballmac/directionCopy the source into your project.
components/ballmac/status-badge-row.tsx// Ballmac UI: Status Badge Row. https://ui.ballmac.com/components/status-badge-row "use client" import * as React from "react" import { CheckCircle2, CircleAlert, OctagonAlert, Wrench } from "lucide-react" import { cn } from "@/lib/utils" import { useLocale, useMessages, defineMessage, type Message, type Msg } from "@/lib/ballmac/i18n" import { useDirection } from "@/lib/ballmac/direction" type ServiceStatus = "operational" | "degraded" | "outage" | "maintenance" type ServiceDay = ServiceStatus | { status: ServiceStatus; note?: string } type ServiceItem = { /** Service name, such as "API". */ name: string /** Current status. */ status: ServiceStatus /** One line under the name. */ description?: string /** Uptime over the history, in percent. Shown with two decimals. */ uptime?: number /** One entry per day, oldest first. */ days?: ServiceDay[] } const META: Record<ServiceStatus, { label: Message; icon: React.ComponentType<{ className?: string }>; bar: string; height: string; chip: string; order: number }> = { operational: { label: defineMessage("status-badge-row.META.operational", "Operational"), icon: CheckCircle2, bar: "bg-chart-2", height: "100%", chip: "border-chart-2/30 bg-chart-2/10", order: 0 }, maintenance: { label: defineMessage("status-badge-row.META.maintenance", "Maintenance"), icon: Wrench, bar: "bg-chart-1", height: "82%", chip: "border-chart-1/30 bg-chart-1/10", order: 1 }, degraded: { label: defineMessage("status-badge-row.META.degraded", "Degraded performance"), icon: CircleAlert, bar: "bg-chart-3", height: "66%", chip: "border-chart-3/30 bg-chart-3/10", order: 2 }, outage: { label: defineMessage("status-badge-row.META.outage", "Outage"), icon: OctagonAlert, bar: "bg-destructive", height: "44%", chip: "border-destructive/30 bg-destructive/10", order: 3 }, } /** "2026-09-30" minus `back` days, as "Sep 21". UTC, so the server and browser agree. */ function dayLabel(locale: string, msg: Msg, endDate: string | undefined, back: number) { const relative = () => (back === 0 ? msg("status-badge-row.today", "Today") : new Intl.RelativeTimeFormat(locale, { numeric: "always" }).format(-back, "day")) if (!endDate) return relative() const t = Date.parse(endDate.length === 10 ? `${endDate}T00:00:00Z` : endDate) if (Number.isNaN(t)) return relative() // UTC, so the server and browser agree. return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(t - back * 86_400_000)) } function normalize(day: ServiceDay) { return typeof day === "string" ? { status: day, note: undefined } : day } /** The text shown for one day, such as "Sep 21 · Outage · Queue backlog". */ function readout(locale: string, msg: Msg, service: ServiceItem, endDate: string | undefined, index: number) { const days = service.days ?? [] const day = normalize(days[index]!) return `${dayLabel(locale, msg, endDate, days.length - 1 - index)} · ${msg.of(META[day.status].label)}${day.note ? ` · ${day.note}` : ""}` } function ServiceRow({ service, endDate }: { service: ServiceItem; endDate?: string }) { const msg = useMessages() const locale = useLocale() const meta = META[service.status] const Icon = meta.icon const [cursor, setCursor] = React.useState<number | null>(null) return ( <li className="grid gap-3 px-4 py-3.5"> <div className="flex items-center gap-3"> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium text-foreground">{service.name}</p> {service.description && <p className="truncate text-xs text-muted-foreground">{service.description}</p>} </div> {cursor !== null && service.days ? ( <span aria-hidden="true" className="truncate rounded-md bg-muted px-2 py-0.5 text-xs text-foreground"> {readout(locale, msg, service, endDate, cursor)} </span> ) : ( service.uptime !== undefined && ( <span className="hidden font-mono text-xs text-muted-foreground tabular-nums sm:block">{msg("status-badge-row.uptime", "{percent}% uptime", { percent: service.uptime.toFixed(2) })}</span> ) )} <span className={cn( "inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium text-foreground", meta.chip )} > <Icon aria-hidden="true" className="size-3.5" /> {msg.of(meta.label)} </span> </div> <HistoryStrip service={service} endDate={endDate} cursor={cursor} setCursor={setCursor} /> </li> ) } function HistoryStrip({ service, endDate, cursor, setCursor, }: { service: ServiceItem endDate?: string cursor: number | null setCursor: React.Dispatch<React.SetStateAction<number | null>> }) { const dir = useDirection() const msg = useMessages() const locale = useLocale() const days = service.days ?? [] const [focused, setFocused] = React.useState(false) if (days.length === 0) return null const last = days.length - 1 const at = cursor ?? last const day = normalize(days[at]!) const text = `${dayLabel(locale, msg, endDate, last - at)}: ${msg.of(META[day.status].label)}${day.note ? `, ${day.note}` : ""}` function onKeyDown(event: React.KeyboardEvent) { const step = event.shiftKey ? 7 : 1 const next = event.key === (dir === "rtl" ? "ArrowRight" : "ArrowLeft") ? at - step : event.key === (dir === "rtl" ? "ArrowLeft" : "ArrowRight") ? at + step : event.key === "Home" ? 0 : event.key === "End" ? last : null if (next === null) return event.preventDefault() setCursor(Math.min(last, Math.max(0, next))) } return ( <div className="relative"> <div role="slider" tabIndex={0} aria-label={msg("status-badge-row.historyDays", "{name} history, {length} days", { name: service.name, length: days.length })} aria-orientation="horizontal" aria-valuemin={0} aria-valuemax={last} aria-valuenow={at} aria-valuetext={text} onKeyDown={onKeyDown} onFocus={() => { setFocused(true) setCursor((c) => c ?? last) }} onBlur={() => { setFocused(false) setCursor(null) }} onPointerLeave={() => !focused && setCursor(null)} className="group/strip flex h-8 items-end gap-px rounded-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > {days.map((raw, i) => { const d = normalize(raw) return ( <span key={i} onPointerEnter={() => setCursor(i)} className="flex h-full min-w-px flex-1 items-end" > <span aria-hidden="true" className={cn( "block w-full rounded-[2px] transition-[opacity,transform] duration-100 motion-reduce:transition-none", META[d.status].bar, cursor !== null && cursor !== i && "opacity-60", cursor === i && "scale-y-[1.08]" )} style={{ height: META[d.status].height }} /> </span> ) })} </div> </div> ) } type StatusBadgeRowProps = Omit<React.ComponentProps<"section">, "title"> & { /** The services to list. */ services: ServiceItem[] /** Heading above the list. */ title?: string /** ISO date of the newest day in each `days` array, for example "2026-09-30". Used to label the bars. */ endDate?: string /** Text under the heading, such as "Updated 2 minutes ago". */ updated?: string /** Replaces the computed banner message. */ summary?: string } function StatusBadgeRow({ services, title, endDate, updated, summary, className, ...props }: StatusBadgeRowProps) { const msg = useMessages() const locale = useLocale() title ??= msg("status-badge-row.title", "System status") const worst = services.reduce<ServiceStatus>((w, s) => (META[s.status].order > META[w].order ? s.status : w), "operational") const bannerText = summary ?? (worst === "operational" ? "All systems operational" : worst === "maintenance" ? "Scheduled maintenance in progress" : worst === "degraded" ? "Some systems are slower than usual" : "Some systems are down") const Banner = META[worst].icon const historyDays = services.reduce((n, s) => Math.max(n, s.days?.length ?? 0), 0) return ( <section data-slot="status-badge-row" data-status={worst} aria-label={title} className={cn("w-full overflow-hidden rounded-xl border bg-card text-card-foreground", className)} {...props} > <header className={cn("flex items-center gap-3 border-b px-4 py-3.5", META[worst].chip)}> <Banner aria-hidden="true" className="size-5 shrink-0" /> <div className="min-w-0 flex-1" role="status"> <h3 className="text-sm leading-5 font-semibold text-foreground">{bannerText}</h3> {updated && <p className="text-xs leading-4 text-muted-foreground">{updated}</p>} </div> </header> <ul className="divide-y"> {services.map((service) => ( <ServiceRow key={service.name} service={service} endDate={endDate} /> ))} </ul> {historyDays > 0 && ( <footer className="flex items-center justify-between border-t bg-muted/30 px-4 py-2 text-xs text-muted-foreground" aria-hidden="true"> <span>{new Intl.RelativeTimeFormat(locale, { numeric: "always" }).format(-historyDays, "day")}</span> <span>{msg("status-badge-row.today", "Today")}</span> </footer> )} </section> ) } export { StatusBadgeRow, type StatusBadgeRowProps, type ServiceItem, type ServiceStatus, type ServiceDay }Update the import paths to match your project setup.
Usage
import { StatusBadgeRow } from "@/components/ballmac/status-badge-row"The full example is in the Code tab above.
Examples
During an incident
Some systems are down
Incident opened 14 minutes ago
- 99.71% uptimeOutage
API
Returning errors for some requests
- 99.90% uptimeDegraded performance
Dashboard
Data may be out of date
- 100.00% uptimeMaintenance
Billing
Scheduled until 18:00 UTC
import { StatusBadgeRow, type ServiceDay } from "@/components/ballmac/status-badge-row"
const calm: ServiceDay[] = Array.from({ length: 45 }, () => "operational")
const rough: ServiceDay[] = Array.from({ length: 45 }, (_, i) => (i === 44 ? { status: "outage", note: "Investigating" } : i === 43 ? "degraded" : i === 20 ? "maintenance" : "operational"))
export default function StatusBadgeRowIncident() {
return (
<div className="w-full max-w-xl">
<StatusBadgeRow
endDate="2026-09-30"
updated="Incident opened 14 minutes ago"
services={[
{ name: "API", status: "outage", uptime: 99.71, days: rough, description: "Returning errors for some requests" },
{ name: "Dashboard", status: "degraded", uptime: 99.9, days: calm, description: "Data may be out of date" },
{ name: "Billing", status: "maintenance", uptime: 100, days: calm, description: "Scheduled until 18:00 UTC" },
]}
/>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
services*The services to list. | ServiceItem[] | — |
titleHeading above the list. | string | — |
endDateISO date of the newest day in each `days` array, for example "2026-09-30". Used to label the bars. | string | — |
updatedText under the heading, such as "Updated 2 minutes ago". | string | — |
summaryReplaces the computed banner message. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Moves through days; Shift moves by a week; Home and End jump to the ends |
| Screen readers | Each history is a slider whose value text reads 'Sep 21: Outage' |
| Color | Status is also shown by icon, word and bar height |
Use with AI
services is [{ name, status, description?, uptime?, days? }]; days holds one status (or { status, note }) per day, oldest first. endDate labels the bars. The banner shows the worst status. Bar height as well as color encodes status. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Status Badge Row (@ballmac/status-badge-row) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Public or internal status pages
- Admin dashboards listing service health
Not for
- A single presence dot (status-dot)
- Charting numeric metrics (chart)
Registry JSON: https://ui.ballmac.com/r/status-badge-row.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
Pairs well with
Status Dot
A compact status indicator that always pairs its color with a visible label.
Badge
A small pill label in four variants with an optional status dot and success, warning and error tones. Style links with badgeVariants().
Chart
Theme-aware chart wrapper for Recharts with token colors, a styled tooltip and legend, a named figure with a screen-reader summary, and keyboard navigation.
API Endpoint
An endpoint reference card: method and path with highlighted parameters, auth, grouped parameter lists, and request and response examples in tabs with copy.