opsinjs
ComponentsData display

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

Theme
Density
Text
Status
badge · base base · style base-lyraOpen under the product theme

Installation

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-pill instead.
  • You are showing a measurement and its unit rather than a count or a word.Use value instead.
  • The chip has to be tapped to do something. A badge is content, not a control, and takes no focus.Use button instead.

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".

Theme
Density
Text
Status
badge-a-count-on-a-label · base base · style base-lyraOpen under the product theme

The 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.

Theme
Density
Text
Status
badge-the-two-weights · base base · style base-lyraOpen under the product theme

Content 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="" sets aria-hidden, so a count already spoken beside the badge is not read twice.
  • srLabel with text sets aria-label and role="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-motion to 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.

KeyActionNotes
TabSkips the badgeA badge is content, not a control. Nothing here is focusable.
Shift+TabSkips the badgeSame in reverse. A badge never appears in the focus order.
Arrow keysNothingThere is nothing to select, move or expand.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below 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.

  • 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.

On this page