Badge
A small neutral label attached to something else, such as a count on a tab or a word on a list row. It takes neither colour axis and never stands in for status.
Preview
badge · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/badge
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { Badge } from "@/components/ui/badge"<span className="inline-flex items-center gap-2">
Messages
<Badge srLabel="3 unread">3</Badge>
</span>When to use it
Use it when
- A small neutral count attached to a label, such as an unread count on a tab, where the number carries no clinical meaning.
- A short neutral word on a list row or a heading, such as a category name the product owns, that is not a status.
- A compact chip of neutral chrome that reads more calmly than a full sentence, as long as it carries content rather than urgency.
Do not use it when
- The label says what a reading means or how urgent it is. A badge would carry that as colour alone.Use
status-pillinstead. - You are showing a measurement and its unit rather than a count or a word.Use
valueinstead. - The chip has to be tapped to do something. A badge is content, not a control, and takes no focus.Use
buttoninstead.
Examples
A count on a label
Each badge attaches a fictional count to the label beside it. srLabel names
that count, so a screen reader hears "5 waiting" rather than a lone "5".
badge-a-count-on-a-label · base base · style base-lyraOpen under the product themeThe two weights
variant chooses soft or outline, and both are neutral. Read in greyscale
they are two quiet chips, and a status pill beside them would still be the only
thing carrying a level.
badge-the-two-weights · base base · style base-lyraOpen under the product themeContent guidelines
Keep a badge to a few characters: a number, or a short neutral word. Give a bare
number a noun through srLabel, so a screen reader hears what it counts. Never
write a clinical status word into a badge. "Steady" and "Needs attention" belong
in a StatusPill, beside the colour and the glyph that make
the level readable.
Do
"3" with srLabel="3 unread" names the count for a screen reader and
stays quiet chrome.
Don’t
A red badge reading "High" beside a reading tells a clinical level by colour alone. That is a StatusPill.
Accessibility
Audited against WCAG 2.2 AA, in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending.
The source pass found one real defect and fixed it: srLabel set aria-label
on a bare span, whose implicit role is generic and does not reliably carry
an author-supplied name to assistive technology, so a caller's count risked
being silently dropped in favour of the visible digit. The badge now pairs
aria-label with role="img" whenever srLabel is a non-empty string, the
same pairing already used on Avatar. Nothing else about the
markup, the props or the rendered output changed.
Checked by pnpm run check:a11y on every commit. The badge draws only the
neutral chrome roles, carries no data-status, and sets its text at the
caption1 semantic step rather than a px size.
Argued from the source, not measured.
srLabel=""setsaria-hidden, so a count already spoken beside the badge is not read twice.srLabelwith text setsaria-labelandrole="img"together, so a bare count is named reliably rather than risking a lone number.- Nothing takes focus, answers a key or animates, so there is no keyboard
contract and nothing for
prefers-reduced-motionto reduce. - At 200% text and under a wider text-spacing override the chip grows with the text rather than clipping it, because nothing fixes its height or hides its overflow.
Residual, disclosed rather than fixed. No contrast pair in either weight
has been measured against real token values, so the numbers below are
whatever the generator has produced so far. Nothing in the component checks
that a caller's srLabel textually contains the visible content, so a caller
who ignores the guidance in Content guidelines could still write a disjoint
label; that stays a documented usage contract rather than enforced behaviour.
Whether a screen reader speaks a labelled badge usefully in the flow of its
host still has not been listened to, only reasoned about from the roles.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the badge | A badge is content, not a control. Nothing here is focusable. |
| Shift+Tab | Skips the badge | Same in reverse. A badge never appears in the focus order. |
| Arrow keys | Nothing | There is nothing to select, move or expand. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from BadgeProps in registry/bases/base/badge.tsx.
variant defaults to soft and chooses a neutral weight only. There is no
colour prop, because a badge is on neither axis, and colour passed through
className is the caller's to keep off both. srLabel names a bare count, and
an empty string hides a decorative badge from the accessibility tree.
Related
- StatusPill carries a clinical level as colour, icon and word together, which a neutral badge cannot.
- Value formats a measurement and its unit. A badge counts or names and formats nothing.
- Callout sets information apart in a block. A badge is a chip attached inline to something else.
Table
Rows and columns in a real semantic table, the readable twin a chart ships beside it, with a required caption, right-aligned numeric columns and its own scroll.
Surface
The primitive behind every panel, sheet and card. It decides how solid or translucent a layer looks, and guarantees that whatever sits on it stays readable.