# Status Badge

Badge that morphs between states: the width animates, the label cross-fades and the dot pulses while live.

**Category:** Feedback  
**Exports:** `StatusBadge`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/status-badge.json
```
The file lands at `components/ui/status-badge.tsx`; import from `@/components/ui/status-badge`.

## API — Status Badge
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `status` | `"idle" \| "pending" \| "live" \| "success" \| "error"` | — |  |
| `children` | `ReactNode` | — | Label; defaults to the status name. |

## Example — Status Badge

```tsx
import * as React from "react";
import { StatusBadge, type Status } from "@/components/ui/status-badge";
import { Button } from "@/components/ui/button";

const flow: Status[] = ["idle", "pending", "success", "live", "error"];

export default function StatusBadgeDemo() {
  const [i, setI] = React.useState(0);
  return (
    <div className="flex flex-col items-center gap-6">
      <StatusBadge status={flow[i]} />
      <Button variant="secondary" size="sm" onClick={() => setI((n) => (n + 1) % flow.length)}>
        Next status
      </Button>
    </div>
  );
}
```

## Source — components/ui/status-badge.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type Status = "idle" | "pending" | "live" | "success" | "error";

const styles: Record<Status, { label: string; dot: string; text: string }> = {
  idle: { label: "Idle", dot: "bg-muted-foreground", text: "text-muted-foreground" },
  pending: { label: "Pending", dot: "bg-foreground/70", text: "text-foreground" },
  live: { label: "Live", dot: "bg-ember", text: "text-foreground" },
  success: { label: "Deployed", dot: "bg-success", text: "text-foreground" },
  error: { label: "Failed", dot: "bg-destructive", text: "text-destructive" },
};

export type StatusBadgeProps = {
  status: Status;
  children?: React.ReactNode;
  className?: string;
};

export function StatusBadge({ status, children, className }: StatusBadgeProps) {
  const reduce = useReducedMotion();
  const s = styles[status];
  const spring = { type: "spring" as const, stiffness: 500, damping: 34 };

  return (
    <motion.span
      layout={!reduce}
      transition={spring}
      role="status"
      className={cn("inline-flex h-7 items-center gap-2 overflow-hidden rounded-full border bg-card px-3 text-xs font-medium", s.text, className)}
    >
      <motion.span layout="position" className="relative grid size-2 place-items-center">
        {status === "live" && !reduce && (
          <motion.span
            className="absolute inset-0 rounded-full bg-ember"
            animate={{ scale: [1, 2.6], opacity: [0.6, 0] }}
            transition={{ repeat: Infinity, duration: 1.4, ease: "easeOut" }}
          />
        )}
        {status === "pending" && !reduce ? (
          <motion.span
            className="size-2 rounded-full border-[1.5px] border-foreground/70 border-t-transparent"
            animate={{ rotate: 360 }}
            transition={{ repeat: Infinity, duration: 0.8, ease: "linear" }}
          />
        ) : (
          <motion.span className={cn("relative size-2 rounded-full transition-colors duration-200", s.dot)} />
        )}
      </motion.span>
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={status}
          layout="position"
          initial={{ y: reduce ? 0 : 12, opacity: 0, filter: "blur(3px)" }}
          animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
          exit={{ y: reduce ? 0 : -12, opacity: 0, filter: "blur(3px)" }}
          transition={spring}
          className="whitespace-nowrap"
        >
          {children ?? s.label}
        </motion.span>
      </AnimatePresence>
    </motion.span>
  );
}
```