← Optimotive UI/components/Status Badge
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-badge

Preview

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 }