Display

Status Badge

A presence and health indicator — colored dot or tinted pill, with status colors mapped to theme tokens and an optional liveness pulse.

Preview in your theme

Loading preview…

import * as React from "react"
import { cn } from "@/lib/utils"

type BuiltInStatus = "online" | "offline" | "busy" | "away"
export type StatusBadgeStatus = BuiltInStatus | "custom"

/**
 * Status → color mapping, kept as one editable data table.
 * Every value is a theme CSS variable so the host palette (and dark mode)
 * decides the actual colors — remap freely per project.
 */
const STATUS_COLORS: Record<BuiltInStatus, string> = {
  online: "var(--chart-2)",
  busy: "var(--chart-1)",

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/status-badge.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "StatusBadge" component — a presence /
health indicator rendered as a colored dot or a tinted pill.

Contract
- Export a forwardRef span extending React.HTMLAttributes<HTMLSpanElement>.
- Props: status: "online" | "offline" | "busy" | "away" | "custom" (required);
  label?: string (visible text — omit for a dot-only badge, screen readers
  still get the status name via an sr-only span); pulse?: boolean (defaults
  to true when status is "online", false otherwise); variant?: "dot" | "pill"
  (default "dot"); customColor?: string (read only when status is "custom" —
  pass a theme token like "var(--primary)"; falls back to var(--primary));
  className merged via cn(), remaining props + style spread on the root span.

Behavior
- Resolve one CSS color per status from a small lookup table:
  online → var(--chart-2), busy → var(--chart-1), away → var(--chart-4),
  offline → var(--muted-foreground); "custom" reads customColor.
- The color reaches the DOM only through inline style — never palette
  classes — so the mapping stays an editable data table, and the host theme
  (including dark mode) decides the actual hues.
- pulse renders an animate-ping clone of the dot behind the solid dot;
  the clone is aria-hidden and motion-reduce:hidden — purely decorative,
  the state stays fully readable with animations off.
- No label → render an sr-only span with the default status name (Online /
  Offline / Busy / Away). The badge is a presentational span; consumers who
  need live change announcements add role="status" at the call site.
- Static markup, no hooks / events / browser APIs — do NOT mark it
  "use client"; it renders fine as a server component.

Rendering & styling
- dot variant: inline-flex gap-2 text-sm; a size-2 rounded-full dot
  (backgroundColor: the resolved color) + label inheriting the surrounding
  text color.
- pill variant: rounded-full px-2.5 py-1 text-xs font-medium capsule;
  background is color-mix(in oklab, <color> 15%, transparent) and text color
  is the full-strength <color> — one variable yields both tint and text, so
  contrast holds in light and dark themes for free.
- The dot wrapper is aria-hidden (color is decoration; text carries meaning).
- Semantic tokens only: var(--chart-N), var(--muted-foreground),
  var(--primary) — no hex, no palette classes.

Customization levers
- Remap status colors: the four lookup-table entries are the single source of
  truth — point online at var(--chart-3) or busy at var(--destructive) and
  every badge follows; no call site changes.
- Size: the dot is size-2 — scale to size-2.5 / size-3 together with the
  pill's px / py / text-* for a bigger badge; only promote it to a `size`
  prop if you need a documented scale.
- Avatar-corner presence recipe: wrap the avatar in a relative container and
  drop a label-less badge at the corner:
    <span className="relative inline-flex">
      <Avatar ... />
      <StatusBadge status="online" className="absolute -bottom-0.5 -right-0.5
        rounded-full ring-2 ring-background" />
    </span>
  The sr-only text keeps the state accessible; the ring separates the dot
  from the avatar on any surface.
- Pulse policy: pass pulse explicitly to detach it from "online" — pulse a
  "deploying" custom status, or force it off everywhere in dense tables.
- New states: add a lookup entry + default label for recurring ones, or use
  status="custom" with a per-call-site token.
- Tint strength: the 15% in color-mix is the only knob for pill background
  intensity; 10–20% is the useful range.

Concepts

  • Status-to-token mapping — one lookup table turns a named state into a theme CSS variable; the palette lives in the theme, so a host restyles every badge by editing four entries (or its tokens), never touching call sites.
  • Tint from one colorcolor-mix(in oklab, <c> 15%, transparent) derives the pill background from the same variable that colors the text, so tint and foreground can never drift apart across light and dark themes.
  • Pulse as liveness — the ping ring means "live right now", which is why it defaults on only for online; it is a decorative aria-hidden layer that disappears under reduced motion without losing the state.
  • Sr-only fallback — a dot-only badge still announces its status name to screen readers; color alone is never the only channel.
  • Presentational by default — the badge is a plain span, not a live region; call sites that swap statuses at runtime opt into announcements with role="status".

On This Page