Badge
Displays a badge, or a component that looks like one — nine variants, including the four semantic status families.
- Status
- Since
0.1.0- Accessibility pattern
- labelled chip, never colour alone
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Badge before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify badgeInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/badgeRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
import { Badge } from "@/components/ui/badge";
<Badge variant="secondary">Badge</Badge>;Examples
Variants
Use the variant prop to change the variant of the badge. Upstream ships six; the four semantic
status families (COL-12) add success, warning and info beside destructive, written in
upstream's own tinted shape.
With Icon
You can render an icon inside the badge. Use data-icon="inline-start" to render the icon on the
leading edge and data-icon="inline-end" to render it on the trailing edge.
With Spinner
You can render a spinner inside the badge. Give the spinner the same data-icon attribute so it
takes the icon spacing.
Link
Use the render prop to render a link as a badge. The badge is a span by default, so the anchor
brought in through render is what carries the link role.
Custom Colors
You can retint a badge by passing classes through className. Upstream's example reaches for the
raw Tailwind palette; use semantic tokens instead, so the override follows the theme into dark mode
and survives a retheme.
RTL
Every variant is written in logical properties, so a right-to-left document mirrors the padding and the icon spacing without any extra work.
Mapping domain statuses
Map each of your domain's statuses to a label and a variant in ONE typed table, and render every
badge from it. BadgeVariant is the variant union, so satisfies rejects a misspelt or retired
variant at compile time and fails the build when a new status has no entry.
| Tone | Use it for |
|---|---|
success | Done and healthy: live, paid, passed |
info | In flight: running, syncing, in review |
warning | Needs attention soon: paused, expiring, stale |
destructive | Failed or blocked: failed, rejected, overdue |
secondary | Not started or neutral: draft, queued |
outline | Out of play: archived, cancelled |
import { Badge, type BadgeVariant } from "@/components/ui/badge";
type RunStatus =
"live" | "running" | "paused" | "failed" | "draft" | "archived";
const RUN_STATUS = {
live: { label: "Live", variant: "success" },
running: { label: "Running", variant: "info" },
paused: { label: "Paused", variant: "warning" },
failed: { label: "Failed", variant: "destructive" },
draft: { label: "Draft", variant: "secondary" },
archived: { label: "Archived", variant: "outline" },
} satisfies Record<RunStatus, { label: string; variant: BadgeVariant }>;
<Badge variant={RUN_STATUS[status].variant}>{RUN_STATUS[status].label}</Badge>;Warning pill
A count that needs attention — missing specs, overdue items — is the warning variant with a
TriangleAlert and the count. Say what is counted when the pill stands alone; the bare number is
enough only where a column header or label already names it. It is the existing Badge: never a
new pill component.
<Badge variant="warning">
<TriangleAlert aria-hidden />
{n} missing specs
</Badge>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "ghost" | "info" | "link" | "outline" | "secondary" | "success" | "warning" | null | — |
Accessibility
- Renders a
span— a badge is a label, not a control, and it is not focusable or clickable on its own. Passrenderto make it an anchor or a button when it has to be operable. - The tint never carries the meaning on its own (A11Y-8): every status badge keeps its word, and an icon beside it is decorative rather than load-bearing.
- Each tinted status variant takes the family's
-textink over its/10fill (A11Y-13), which clears the AA contrast floor in both themes. - The component carries no focus ring of its own. When
rendermakes it operable, the global:focus-visibleoutline inbase.cssapplies (FOC-1, FOC-6).
| Contract | States tested |
|---|---|
| Behaviour | default, error, loading, success |
| Accessibility | busy |
| Visual | default, loading, error, success |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/badge.patch. Every hunk:
- FOC-1, FOC-6 — the
focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50glow and thearia-invalidrings are removed;base.cssowns focus outright. - FOC-14 — the invalid border stays upstream's
aria-invalid:border-destructive, held in every state, focused or not (FOC-14 retired FOC-5'snot-focus:guard). - COL-12 —
success,warningandinfovariants, in upstream's owndestructiveshape. - A11Y-13 — each tinted status variant takes the family's
-textink;bg-<family>/10undertext-<family>measures between 3.98:1 and 4.35:1, under the AA floor A11Y-1 enforces. - A11Y-8 — a badge is a labelled chip: the tint never carries the meaning alone.
- DOC-2 —
cnis imported from@vegastack/design. - API-16 —
"use client"is added:useRendercallsReact.useRefinternally, so the module throws on import in a React Server Component without the boundary. - A11Y-2 — an interactive badge (rendered as a link or a button) gets an invisible
::afterhit area and dropsoverflow-hidden, so its pointer target clears 24px. Upstream's is 20px tall. - API-25 —
BadgeVariant, the variant union derived frombadgeVariants, is exported so a status map is typed against the system's own variants.