Badge

A small label for status, categories, or counts. Available in seven tonal variants and two sizes, sizing any icon child for you and rendering as whatever element you hand it.

Variants

primary is a kept alias of default — the two render the same classes.

DefaultPrimarySecondaryOutlineSuccessWarningDestructive

Sizes

SmallMedium

With an icon

Any direct <svg> child is sized to 12px and made pointer-events-none by the badge itself, and the 4px gap is already there — drop the icon in without a size prop and it lines up with the label.

ApprovedRejectedPendingNew

Render as another element

asChild merges the badge classes onto its single child instead of rendering a <span>. Use it when the badge is genuinely interactive — a tag link, or a filter chip that is itself the button — so the element carries the right semantics and keyboard behaviour rather than a click handler on a span. A chip with a separate ✕ is the next section.

design

Removable

New in 3.7.0. onRemove adds a ✕ button at the end of the badge, for chips that can be taken away — watchers, applied labels. Pass removeLabel with it: the button has only an icon, so its name is the one place that says what it removes, and that sentence is yours, not the package's. badge.tsx stays a static file, so a removable badge still renders in a Server Component — only the handler needs a client.

LanMinhQuân

Use cases

When a row of badges can outgrow its space — labels on a task card, tags in a table cell — BadgeOverflow folds the ones that do not fit into a “+N”.

Status:Active
Plan:Pro
Notifications:3
Category:Design

Props

PropTypeDefaultDescription
variant'default' | 'primary' | 'secondary' | 'outline' | 'success' | 'warning' | 'destructive''default'Visual style of the badge. primary is an alias of default and renders identically.
size'sm' | 'md''md'Controls padding and font size — sm is 10px text, md is 12px.
asChildbooleanfalseRenders the single child element with the badge classes merged onto it (Radix Slot) instead of a <span>. Ignored when onRemove is set — the ✕ button needs a span to sit in.
onRemove() => void—New in 3.7.0. Draws a ✕ button at the end of the badge — a removable chip (a watcher, an applied label). The button fades with opacity, never with an alpha colour on the icon.
removeLabelstring—Accessible name of the ✕ button. There is deliberately no default: an icon-only button's name is the only thing that says what it removes (“Remove Lan from watchers”), which is the app's sentence — and a shared “Remove” repeated down a list is N buttons with one name. Leave it out next to onRemove and the package warns in development.
removeDisabledbooleanfalseDisables only the ✕ (say, while the removal saves); the badge itself still shows normally.
classNamestring—Additional Tailwind classes. Merged after the variant classes, so anything you pass here wins.
...restReact.ComponentProps<'span'>—All native span props are forwarded.

The cva recipe is exported as badgeVariants for the rare case where you need the classes without the element.

How the tonal variants are built

Badge text is 10–12px, so it has to clear AA 4.5:1 against the very tint it sits on. Each tonal variant therefore pairs a background token with the matching ink token — never the background token used as a text colour:

  • success — bg-green/10 (dark: /20) with text-green-ink.
  • warning — bg-yellow/10 with text-yellow-ink.
  • destructive — bg-red/10 with text-destructive-ink.
  • outline — a transparent fill with border-control-edge, not border-border: with no fill the border is the only thing outlining the badge, so it owes the 3:1 non-text threshold that the decorative border tier (1.16:1) does not meet.

Because the ink tokens flip with the theme, no variant needs a dark: text override. If you tint a badge yourself via className, /20 is the darkest tint that stays safe for every tone in both themes.

A tint is see-through, so whatever lies under the badge shows through it — harmless on a flat surface, not on a line. A badge that straddles a card's border, like the “Most popular” flag of a pricing card, lets the border run through its label in dark mode, where the tints are 20% alpha. Give it an opaque backing in the colour of the surface it belongs to: wrap it in a <span className="flex bg-card rounded-md"> that carries the positioning — flex, so the backing is exactly as tall as the badge rather than a line box — and the tint reads exactly as it does inside the card.

Accessibility

  • Badge is a span — purely decorative unless given a role.
  • For notification counts, consider aria-label or visually hidden text.
  • Color alone should not convey meaning — pair with text or icons.
  • If the badge is clickable, use asChild over a real <a> or <button> so it is focusable and reachable by keyboard.
  • A removable badge's ✕ is a real <button type="button"> named by removeLabel. Name it for the thing it removes, so a list of chips is not a list of identical “Remove” buttons.