Web InnoventixFreeCode

Status Badge

Original · free

status pills (active/pending/error)

byWeb InnoventixReact + Tailwind
badgestatusbadges
Open

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.json
badge-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 quote

Similar components

Browse all →