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…
- Registry dependencies
- utils
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/status-badge.jsonPrompt
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 the text is
color-mix(in oklab, <color> 60%, var(--foreground)) — one variable yields
both tint and text. The raw token over its own tint drops below 4.5:1 for
mid-lightness colors (a 12px label needs AA), and pulling it toward
foreground darkens it in light mode and lightens it in dark mode (with the
default tokens the weakest pill goes from 3.07:1 to 6.85:1). The dot keeps
the full-strength <color>.
- 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.
- Text strength: the 60% in the text color-mix trades hue for contrast;
raise it only after re-checking 4.5:1 against the tinted background in
both themes.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 color —
color-mix(in oklab, <c> 15%, transparent)derives the pill background, andcolor-mix(in oklab, <c> 60%, var(--foreground))the pill text, from the same variable, so tint and text can never drift apart across light and dark themes, and pulling the text toward foreground keeps a 12px label above 4.5:1 on its own tint. - 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".