Status badge shadcn ships a Badge with four variants and none of them is a status. It has --destructive, which means a destructive action, not a system on fire. This is what you add to close that gap. npx shadcn@latest add badge npm i class-variance-authority clsx tailwind-merge lucide-react Tokens this needs: --card, --card-foreground, --muted-foreground, --border, --status-critical, --status-warn, --status-nominal, --status-unknown The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // status.ts ──────────────────────────────────────────────────────────────────────── import type { LucideIcon } from "lucide-react"; import { AlertTriangle, CheckCircle2, CircleHelp, OctagonAlert } from "lucide-react"; /** * The closed set. * * Adding a state means changing this union, which is the whole point: the * compiler refuses anything that is not one of these four, so a fifth status * cannot quietly appear at a call site six months from now. "How many states, * and is the set closed and written down?" stops being a review question. */ export const STATUSES = ["nominal", "warn", "critical", "unknown"] as const; export type Status = (typeof STATUSES)[number]; /** * Every state carries a non-colour signal. * * This makes "is there a non-colour signal for every state" structural rather * than advisory—there is no way to render a status without also rendering its * icon and its label, because the component reads both from here. * * `unknown` is deliberately not a quiet success. No data and healthy are * different answers and they must not look alike. */ export const STATUS_META: Record = { nominal: { label: "Nominal", Icon: CheckCircle2 }, warn: { label: "Warning", Icon: AlertTriangle }, critical: { label: "Critical", Icon: OctagonAlert }, unknown: { label: "No data", Icon: CircleHelp }, }; ──────────────────────────────────────────────────────────────────────── // StatusBadge.tsx ──────────────────────────────────────────────────────────────────────── import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils"; import { STATUS_META, type Status } from "./status"; /** * Severity is carried by three things at once: hue, lightness, and an icon. * Hue alone fails in greyscale and fails for the common colour vision * deficiencies, both of which are checklist items on this pattern. * * The tokens are --status-*, never --destructive. shadcn's --destructive means * "this button deletes things"; a critical alarm is a state of the world. They * often land on similar reds and they are not the same semantic. */ const statusBadge = cva( "inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium", { variants: { status: { nominal: "border-status-nominal/30 bg-status-nominal/10 text-status-nominal", warn: "border-status-warn/30 bg-status-warn/10 text-status-warn", critical: "border-status-critical/40 bg-status-critical/10 text-status-critical", unknown: "border-status-unknown/30 bg-status-unknown/10 text-status-unknown", }, }, defaultVariants: { status: "unknown" }, }, ); export function StatusBadge({ status, className }: { status: Status; className?: string }) { const { label, Icon } = STATUS_META[status]; return ( ); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { STATUSES } from "./status"; import { StatusBadge } from "./StatusBadge"; /** The closed set, each value beside the rule that fires it. */ const FIRES_WHEN = { nominal: "p95 under 300ms", warn: "300ms to 800ms", critical: "over 800ms", unknown: "no sample for 5m", } as const; export default function Demo() { return (
StateFires when
); }