Skip to content
Andrew SmithSolutions
← Component library

Badge

Data display

The mono uppercase chip used for tags and states — the eyebrow treatment shrunk to sit inline.

TagStateMonoCSS only — no dependency

Preview

Shipped

Controls

Notes

Tone is decorative. If the badge is the only thing distinguishing a failed row from a passing one, the row is under-labelled — say the word, do not just change the colour.

Text stays on --ink or --ink-muted rather than the tone colour, so every tone clears contrast without a per-tone audit.

Source

Show
import { cn } from "@/lib/cn";

const tones = {
  neutral: "border-rule bg-paper-raised text-ink-muted",
  accent: "border-accent/35 bg-accent-soft text-ink",
  sunken: "border-rule-strong bg-paper-sunken text-ink-muted",
} as const;

const dots = {
  neutral: "bg-ink-muted",
  accent: "bg-accent",
  sunken: "bg-ink-faint",
} as const;

export type BadgeTone = keyof typeof tones;

export function Badge({
  tone = "neutral",
  dot = false,
  className,
  children,
}: {
  tone?: BadgeTone;
  dot?: boolean;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <span
      className={cn(
        "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1",
        "font-mono text-[11px] uppercase leading-none tracking-[0.12em]",
        tones[tone],
        className,
      )}
    >
      {/* Decorative. The label carries the meaning; the dot only repeats it. */}
      {dot && <span className={cn("size-1.5 rounded-full", dots[tone])} aria-hidden="true" />}
      {children}
    </span>
  );
}

export default function Demo({
  tone = "neutral",
  label = "Shipped",
  dot = false,
}: {
  tone?: BadgeTone;
  label?: string;
  dot?: boolean;
}) {
  return (
    <Badge tone={tone} dot={dot}>
      {label}
    </Badge>
  );
}

Next step

Tell me where the business is losing time.

No pitch deck and no discovery-call funnel. Describe the bottleneck and I will tell you plainly whether it is worth building something for, and roughly what that would take.