Status Badge
Original · freestatus pills (active/pending/error)
byWeb InnoventixReact + Tailwind
badgestatusbadges
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/badge-status.jsonbadge-status.tsx
"use client";
import { useMemo, useRef, useState, type KeyboardEvent } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type Status = "active" | "pending" | "error";
type Filter = Status | "all";
type Service = {
id: string;
name: string;
detail: string;
region: string;
status: Status;
};
type StatusStyle = {
label: string;
caption: string;
pill: string;
dot: string;
glow: string;
softText: string;
};
const STATUS_ORDER: readonly Status[] = ["active", "pending", "error"] as const;
const STATUS_META: Record<Status, StatusStyle> = {
active: {
label: "Active",
caption: "Operational",
pill: "bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/25",
dot: "bg-emerald-500 dark:bg-emerald-400",
glow: "bg-emerald-400/70 dark:bg-emerald-300/60",
softText: "text-emerald-600 dark:text-emerald-400",
},
pending: {
label: "Pending",
caption: "Degraded",
pill: "bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/25",
dot: "bg-amber-500 dark:bg-amber-400",
glow: "bg-amber-400/70 dark:bg-amber-300/60",
softText: "text-amber-600 dark:text-amber-400",
},
error: {
label: "Error",
caption: "Outage",
pill: "bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/25",
dot: "bg-rose-500 dark:bg-rose-400",
glow: "bg-rose-400/70 dark:bg-rose-300/60",
softText: "text-rose-600 dark:text-rose-400",
},
};
const INITIAL_SERVICES: readonly Service[] = [
{ id: "gw", name: "API Gateway", detail: "Edge request routing & rate limits", region: "us-east-1", status: "active" },
{ id: "auth", name: "Authentication", detail: "OAuth exchange & session tokens", region: "eu-west-1", status: "active" },
{ id: "db", name: "Postgres Primary", detail: "Failover promotion in progress", region: "us-east-1", status: "pending" },
{ id: "hooks", name: "Webhook Delivery", detail: "Retry queue backlog rising", region: "ap-south-1", status: "error" },
{ id: "cdn", name: "CDN & Assets", detail: "Static delivery, global edge", region: "global", status: "active" },
{ id: "bill", name: "Billing Worker", detail: "Metering jobs draining", region: "us-west-2", status: "pending" },
];
const FILTERS: readonly { value: Filter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "active", label: "Active" },
{ value: "pending", label: "Pending" },
{ value: "error", label: "Error" },
];
function nextStatus(current: Status): Status {
const index = STATUS_ORDER.indexOf(current);
return STATUS_ORDER[(index + 1) % STATUS_ORDER.length];
}
function StatusIcon({ status }: { status: Status }) {
const shared = "h-4 w-4";
if (status === "active") {
return (
<svg viewBox="0 0 20 20" fill="none" className={shared} aria-hidden="true">
<circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.6" />
<path d="M6.6 10.2l2.2 2.2 4.6-4.8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
if (status === "pending") {
return (
<svg viewBox="0 0 20 20" fill="none" className={shared} aria-hidden="true">
<circle cx="10" cy="10" r="7.5" stroke="currentColor" strokeWidth="1.6" />
<path d="M10 6v4.2l2.8 1.9" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
return (
<svg viewBox="0 0 20 20" fill="none" className={shared} aria-hidden="true">
<path d="M10 2.6L18 16.4H2L10 2.6z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
<path d="M10 8v3.4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<circle cx="10" cy="13.7" r="0.95" fill="currentColor" />
</svg>
);
}
export default function BadgeStatus() {
const prefersReduced = useReducedMotion();
const [services, setServices] = useState<Service[]>(() => INITIAL_SERVICES.map((s) => ({ ...s })));
const [filter, setFilter] = useState<Filter>("all");
const [announce, setAnnounce] = useState("");
const filterRefs = useRef<(HTMLButtonElement | null)[]>([]);
const counts = useMemo(
() => ({
active: services.filter((s) => s.status === "active").length,
pending: services.filter((s) => s.status === "pending").length,
error: services.filter((s) => s.status === "error").length,
}),
[services],
);
const overall: Status = counts.error > 0 ? "error" : counts.pending > 0 ? "pending" : "active";
const overallText: Record<Status, string> = {
active: "All systems operational",
pending: "Degraded performance",
error: "Partial outage detected",
};
const visible = filter === "all" ? services : services.filter((s) => s.status === filter);
function cycle(service: Service) {
const upcoming = nextStatus(service.status);
setServices((prev) => prev.map((s) => (s.id === service.id ? { ...s, status: upcoming } : s)));
setAnnounce(`${service.name} set to ${STATUS_META[upcoming].label}, ${STATUS_META[upcoming].caption}.`);
}
function onFilterKeyDown(event: KeyboardEvent<HTMLButtonElement>, index: number) {
const last = FILTERS.length - 1;
let target = index;
if (event.key === "ArrowRight" || event.key === "ArrowDown") target = index === last ? 0 : index + 1;
else if (event.key === "ArrowLeft" || event.key === "ArrowUp") target = index === 0 ? last : index - 1;
else if (event.key === "Home") target = 0;
else if (event.key === "End") target = last;
else return;
event.preventDefault();
setFilter(FILTERS[target].value);
filterRefs.current[target]?.focus();
}
const cardMotion = prefersReduced
? { initial: false as const, animate: { opacity: 1 }, exit: { opacity: 0 } }
: {
initial: { opacity: 0, y: 10, scale: 0.98 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, y: -8, scale: 0.98 },
};
return (
<section
aria-labelledby="bsx-heading"
className="relative w-full overflow-hidden bg-slate-50 px-6 py-20 text-slate-900 sm:py-24 dark:bg-slate-950 dark:text-slate-100"
>
<style>{`
@keyframes bsx-ping { 0% { transform: scale(1); opacity: .7; } 75%, 100% { transform: scale(2.4); opacity: 0; } }
@keyframes bsx-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
.bsx-ping { animation: bsx-ping 1.7s cubic-bezier(0, 0, 0.2, 1) infinite; }
.bsx-breathe { animation: bsx-breathe 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.bsx-ping, .bsx-breathe { animation: none !important; }
}
`}</style>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 -top-24 mx-auto h-64 max-w-3xl rounded-full bg-gradient-to-r from-indigo-200/40 via-sky-200/30 to-emerald-200/40 blur-3xl dark:from-indigo-500/10 dark:via-sky-500/10 dark:to-emerald-500/10"
/>
<div className="relative mx-auto max-w-4xl">
<header className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
System status
</p>
<h2 id="bsx-heading" className="mt-2 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Service health board
</h2>
<p className="mt-2 max-w-md text-sm text-slate-600 dark:text-slate-400">
Live status pills for every core service. Select a state to filter, or click any pill to advance it through
active, pending and error.
</p>
</div>
<div
className={`inline-flex items-center gap-2.5 self-start rounded-full px-4 py-2 text-sm font-semibold ring-1 ring-inset sm:self-auto ${STATUS_META[overall].pill}`}
>
<span className="relative flex h-2.5 w-2.5">
{!prefersReduced && (
<span className={`absolute inline-flex h-full w-full rounded-full bsx-ping ${STATUS_META[overall].glow}`} />
)}
<span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${STATUS_META[overall].dot}`} />
</span>
{overallText[overall]}
</div>
</header>
<div className="mt-8 flex flex-wrap items-center gap-3">
<div
role="radiogroup"
aria-label="Filter services by status"
className="inline-flex rounded-xl bg-slate-100 p-1 ring-1 ring-slate-200 dark:bg-slate-900 dark:ring-slate-800"
>
{FILTERS.map((item, index) => {
const selected = filter === item.value;
return (
<button
key={item.value}
ref={(el) => {
filterRefs.current[index] = el;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => setFilter(item.value)}
onKeyDown={(event) => onFilterKeyDown(event, index)}
className="relative rounded-lg px-3.5 py-1.5 text-sm font-medium text-slate-600 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 hover:text-slate-900 aria-checked:text-slate-900 dark:text-slate-400 dark:hover:text-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-900 dark:aria-checked:text-white"
>
{selected && (
<motion.span
layoutId="bsx-filter-pill"
transition={prefersReduced ? { duration: 0 } : { type: "spring", stiffness: 480, damping: 38 }}
className="absolute inset-0 rounded-lg bg-white shadow-sm ring-1 ring-slate-200 dark:bg-slate-700 dark:ring-slate-600"
/>
)}
<span className="relative z-10">{item.label}</span>
</button>
);
})}
</div>
<div className="flex flex-wrap items-center gap-2 text-xs font-medium">
{STATUS_ORDER.map((status) => (
<span
key={status}
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 ring-1 ring-inset ${STATUS_META[status].pill}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_META[status].dot}`} />
{counts[status]} {STATUS_META[status].label.toLowerCase()}
</span>
))}
</div>
</div>
<motion.ul layout={!prefersReduced} className="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2">
<AnimatePresence mode="popLayout" initial={false}>
{visible.map((service) => {
const meta = STATUS_META[service.status];
return (
<motion.li
key={service.id}
layout={!prefersReduced}
initial={cardMotion.initial}
animate={cardMotion.animate}
exit={cardMotion.exit}
transition={{ duration: 0.24, ease: "easeOut" }}
className="group relative flex items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition-colors hover:border-slate-300 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700"
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="truncate text-sm font-semibold text-slate-900 dark:text-white">{service.name}</h3>
<span className="rounded border border-slate-200 px-1.5 py-0.5 font-mono text-[10px] text-slate-500 dark:border-slate-700 dark:text-slate-400">
{service.region}
</span>
</div>
<p className={`mt-1 truncate text-xs ${meta.softText}`}>{service.detail}</p>
</div>
<button
type="button"
onClick={() => cycle(service)}
aria-label={`${service.name} status: ${meta.label}, ${meta.caption}. Activate to change status.`}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold ring-1 ring-inset outline-none transition-transform focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-95 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-900 ${meta.pill}`}
>
<span className="relative flex h-2 w-2" aria-hidden="true">
{!prefersReduced && service.status === "active" && (
<span className={`absolute inline-flex h-full w-full rounded-full bsx-ping ${meta.glow}`} />
)}
<span
className={`relative inline-flex h-2 w-2 rounded-full ${meta.dot} ${
!prefersReduced && service.status === "pending" ? "bsx-breathe" : ""
}`}
/>
</span>
<span aria-hidden="true" className="opacity-90">
<StatusIcon status={service.status} />
</span>
{meta.label}
</button>
</motion.li>
);
})}
</AnimatePresence>
</motion.ul>
{visible.length === 0 && (
<p className="mt-6 rounded-2xl border border-dashed border-slate-300 bg-white/60 px-4 py-8 text-center text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-400">
No services in this state right now.
</p>
)}
<p className="mt-6 text-xs text-slate-500 dark:text-slate-500">
Tip: click any status pill to cycle it — states advance active → pending → error.
</p>
<div aria-live="polite" role="status" className="sr-only">
{announce}
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Badge
Originalbasic badges across colours

Dot Badge
Originaldot status badges

Count Badge
Originalnotification count badges

Pill Badge
Originalsoft pill badges

Outline Badge
Originaloutline badges

Gradient Badge
Originalgradient badges
Icon Badge
Originalbadges with leading icons

Removable Badge
Originalremovable chip badges

Input Chip
Originalchip input tokens

Filter Chip
Originalselectable filter chips

Cloud Tag
Originalweighted tag cloud

Notification Badge
Originalicon with notification badge

