Skip to content
Component installs need the registry setup
VegaStack Design

Badge

Displays a badge, or a component that looks like one — nine variants, including the four semantic status families.

Status
stable
Since
0.1.0
Accessibility pattern
labelled chip, never colour alone

Last updated

BadgeSecondaryDestructiveOutline

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 badge

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/badge

Run 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>;
BadgeSecondaryDestructiveOutline

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.

DefaultSecondaryDestructiveSuccessWarningInfoOutlineGhostLink

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.

VerifiedBookmark

With Spinner

You can render a spinner inside the badge. Give the spinner the same data-icon attribute so it takes the icon spacing.

DeletingGenerating

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.

Solid successSolid infoOutlined warningSquare corners

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.

BadgeSecondaryDestructiveVerifiedBookmark
شارةثانويمدمرمتحققإشارة مرجعية

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.

ToneUse it for
successDone and healthy: live, paid, passed
infoIn flight: running, syncing, in review
warningNeeds attention soon: paused, expiring, stale
destructiveFailed or blocked: failed, rejected, overdue
secondaryNot started or neutral: draft, queued
outlineOut 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>;
LiveRunningPausedFailedDraftArchived

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>
33 missing specs

API Reference

PropTypeDefaultDescription
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. Pass render to 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 -text ink over its /10 fill (A11Y-13), which clears the AA contrast floor in both themes.
  • The component carries no focus ring of its own. When render makes it operable, the global :focus-visible outline in base.css applies (FOC-1, FOC-6).
ContractStates tested
Behaviourdefault, error, loading, success
Accessibilitybusy
Visualdefault, loading, error, success

Do / Don't

Do
Keep a word inside a status badge — Success, Failed, Pending.
Don't
Ship a bare coloured dot or an icon-only badge and expect the colour to carry the status.

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/50 glow and the aria-invalid rings are removed; base.css owns 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's not-focus: guard).
  • COL-12 — success, warning and info variants, in upstream's own destructive shape.
  • A11Y-13 — each tinted status variant takes the family's -text ink; bg-<family>/10 under text-<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 — cn is imported from @vegastack/design.
  • API-16 — "use client" is added: useRender calls React.useRef internally, 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 ::after hit area and drops overflow-hidden, so its pointer target clears 24px. Upstream's is 20px tall.
  • API-25 — BadgeVariant, the variant union derived from badgeVariants, is exported so a status map is typed against the system's own variants.

On this page