Skip to content
Primitiv home
Framework
Consumption mode

Badge

stableSource Figma

A small status/count indicator attached to another element or beside a heading. Read-only, never interactive. Four semantic tones × two treatments (a low-emphasis label chip, a high-emphasis counter chip), sized xs–xl; data-density scales each size further.

Playground

Density

Preview

Stable
Size
Variant
Tone
import { Badge } from "@/components/ui/badge";
<Badge size="md" variant="label" tone="success">Stable</Badge>

Density is set by a data-density ancestor — the Context system, not a Badge prop.

Installation

npx primitiv add badge

Import

import { Badge } from "@/components/ui/badge";

No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.

Props

Badge

Extends HTMLSpanElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultDescription
asChildbooleanRender the single child element instead of a wrapping <span>, merging the badge classes onto it — e.g. <Badge asChild><a>...</a></Badge>.
tone"success" | "warning" | "info" | "danger"successSemantic colour.
variant"label" | "counter"labelVisual treatment. label is the low-emphasis status-word chip; counter is the high-emphasis count chip.
size"xs" | "sm" | "md" | "lg" | "xl"mdBadge size; data-density scales each size further.

Styling contract

--primitiv-badge-background--primitiv-badge-foreground--primitiv-badge-height--primitiv-badge-padding-inline--primitiv-badge-font-family--primitiv-badge-font-size--primitiv-badge-font-weight--primitiv-badge-line-height--primitiv-badge-radius

Accessibility

  • A badge is read-only. It renders a <span>, takes no focus and handles no events — if you need something clickable that looks like this, use Chip, which is built for it, rather than putting a handler on a badge.
  • The tone is not the message. Colour alone carries no meaning for a screen-reader user or anyone who cannot distinguish the hues, so the text has to say it: Failing in the danger tone, not a bare red dot.
  • A counter needs its own words. 8 on its own is announced as "8" — give the surrounding control the context, with an aria-label like "8 unread messages" on the element the badge is attached to.
  • Under asChild the classes merge onto your element and its semantics stay yours — a <time> is still a <time>. Nothing about the badge overrides the role, which is why it is the right hook for marking up a date or an abbreviation.
  • Badge has no neutral tone by design, so a status is always one of four meanings. Reach for Tag when the label is a category rather than a state.

Examples

Tones

Four tones, and the set is deliberately closed: success, warning, info and danger all mean something, so a badge cannot be used as decoration. There is no neutral tone — if what you want is a plain label with no status attached, that is Tag, which carries the neutral the Badge palette leaves out.

Density
SuccessWarningInfoDanger
import { Badge } from "@/components/ui/badge";
<Badge tone="success">Success</Badge><Badge tone="warning">Warning</Badge><Badge tone="info">Info</Badge><Badge tone="danger">Danger</Badge>

Counter

variant="counter" is the number form — a pill sized for one or two digits rather than a word, for unread counts and totals beside a heading or an icon. It is the same component, so a count still carries a tone: an unread count is info, a failing-checks count is danger.

Density
83
import { Badge } from "@/components/ui/badge";
<Badge variant="counter" tone="info">8</Badge><Badge variant="counter" tone="danger">3</Badge>

Sizes and density

Five sizes, and every one rescales again with the nearest data-density ancestor — a badge beside a heading wants lg, one in a table row wants xs. Change the density above and the whole ramp shifts: that is the Context system, not a Badge prop.

Density
StableStableStableStableStable
import { Badge } from "@/components/ui/badge";
<div data-density="comfortable">  <Badge size="xs">Stable</Badge>  <Badge size="sm">Stable</Badge>  <Badge size="md">Stable</Badge>  <Badge size="lg">Stable</Badge>  <Badge size="xl">Stable</Badge></div>

As another element (asChild)

asChild merges the badge's classes onto your own element instead of wrapping a <span> — useful for an <abbr> or a <time> that should read as a badge. It is not a way to make a badge clickable: a badge is read-only by design, and a link or button that looks like one should be a Chip, which is built to be interactive.

Density
import { Badge } from "@/components/ui/badge";
<Badge asChild tone="info">  <time dateTime="2026-08-24">Aug 24</time></Badge>