← Component library
Badge
Data displayThe 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
ShowHide
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
DisclosureNext 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.