component
Status Badge
Colored pill badge for agent/system statuses — active, idle, busy, error, pending, offline.
Installation
npx shadcn@latest add https://optimotive-ui.dev.optimotive-tools.co.uk/registry.json status-badgePreview
Source
components/status-badge.tsx
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type Status = "active" | "idle" | "busy" | "error" | "pending" | "offline"
export interface StatusBadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof statusBadgeVariants> {
/** The semantic status value — drives color and default label */
status: Status
/** Override the auto-generated label */
label?: string
/** Show the animated pulse dot (default: true) */
showDot?: boolean
}
// ---------------------------------------------------------------------------
// Config — single source of truth per status
// ---------------------------------------------------------------------------
export const STATUS_CONFIG: Record<Status, { label: string; dot: string; badge: string }> = {
active: {
label: "Active",
dot: "bg-emerald-500",
badge: "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950 dark:text-emerald-300 dark:border-emerald-800",
},
idle: {
label: "Idle",
dot: "bg-blue-400",
badge: "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800",
},
busy: {
label: "Busy",
dot: "bg-amber-400",
badge: "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-800",
},
error: {
label: "Error",
dot: "bg-red-500",
badge: "bg-red-50 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-800",
},
pending: {
label: "Pending",
dot: "bg-violet-400",
badge: "bg-violet-50 text-violet-700 border-violet-200 dark:bg-violet-950 dark:text-violet-300 dark:border-violet-800",
},
offline: {
label: "Offline",
dot: "bg-zinc-400",
badge: "bg-zinc-100 text-zinc-500 border-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:border-zinc-700",
},
}
/** Statuses whose dot animates with a CSS ping */
const PULSING_STATUSES: Status[] = ["active", "busy", "pending"]
// ---------------------------------------------------------------------------
// CVA variants — size only; status drives color via STATUS_CONFIG
// ---------------------------------------------------------------------------
const statusBadgeVariants = cva(
"inline-flex items-center gap-1.5 rounded-full border font-medium leading-none select-none",
{
variants: {
size: {
sm: "px-2 py-0.5 text-[10px]",
md: "px-2.5 py-1 text-xs",
lg: "px-3 py-1.5 text-sm",
},
},
defaultVariants: { size: "md" },
}
)
const dotSizeMap = {
sm: "size-1.5",
md: "size-2",
lg: "size-2.5",
} as const
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* StatusBadge
*
* A semantic pill badge that conveys agent/task status at a glance.
* Active, busy, and pending states display a pulsing dot indicator.
*
* ### Usage
* ```tsx
* <StatusBadge status="busy" />
* <StatusBadge status="active" size="lg" label="Running" />
* <StatusBadge status="error" showDot={false} />
* <StatusBadge status="offline" size="sm" />
* ```
*/
const StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(
({ status, label, size = "md", showDot = true, className, ...props }, ref) => {
const config = STATUS_CONFIG[status]
const isPulsing = PULSING_STATUSES.includes(status)
const dotSize = dotSizeMap[size ?? "md"]
return (
<span
ref={ref}
role="status"
aria-label={`Status: ${label ?? config.label}`}
className={cn(statusBadgeVariants({ size }), config.badge, className)}
{...props}
>
{showDot && (
<span className={cn("relative flex shrink-0", dotSize)}>
{isPulsing && (
<span
className={cn(
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-60",
config.dot
)}
/>
)}
<span
className={cn("relative inline-flex rounded-full", dotSize, config.dot)}
/>
</span>
)}
{label ?? config.label}
</span>
)
}
)
StatusBadge.displayName = "StatusBadge"
export { StatusBadge, statusBadgeVariants }