Bridge view
Badge
A compact standalone marker that announces status, count, or category alongside another element — a list-item count, a navigation badge, a status dot on an avatar, an inline severity tag. Read-only by canonical contract; the host element handles interactivity.
Also called Chip (when interactive variant) Pill Tag (when categorical)
When to use
Use
For announcing status, count, or category alongside another element. Anchored on the host (next to a label, on top of an avatar, inside a nav item). Read-only — the host carries any interactivity.
Avoid
For interactive selection — that is `Tag Input` (free-form text-bound) or `Segmented Control` (mutex selection). For removable chips driven by user input — that is `Tag Input`. For full-text callouts that span a row — that is `Alert` or `Banner`. For standalone urgent notifications that interrupt the flow — that is `Toast` or `Modal`.
Versus related
- tag-input
`Tag Input` accepts user-typed values as discrete chips; the consumer's value-set drives the chip list. `Badge` is read-only, declarative, and announces metadata about a host element rather than holding form state.
- tile
`Tile` is a 2D grid item with image-led content; `Badge` is an inline marker on a host element. A grid of tiles may carry per-tile badges (selection-count, status), but a wall of badges is not a layout pattern.
- alert
`Alert` is a row-spanning inline message with a body, an icon, and optional actions. `Badge` is a glyph-or-short- label marker that does not own a row. The decision test: does the message need its own block or inline next to a host?
- icon
`Icon` is the SVG-glyph primitive that may sit inside a Badge as the icon-leading slot (severity glyph, status cue). The Icon is decorative when paired with a visible Badge label, meaningful when the Badge is dot-only — in the latter case the accessible name moves to the Badge root via `aria-label`.
Badge is the canonical compact status / count primitive — a small surface that announces a piece of metadata about its host without competing for the host's affordance. Five severity variants (default, success, warning, error, info) cover the canonical announcement ladder; the dot property collapses the surface to a glyph-only indicator. Common hosts: list items, nav items, avatars, buttons (notification count), tabs (unread count). The reference documents the slot anatomy, the live-region contract for changing counts, the color-only-meaning anti-pattern, and the divergence from Tag Input (entry-bound) and Tile (image-led container).
Implementations
How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.
Badge import { Badge } from '@radix-ui/themes';
{/* Default soft badge */}<Badge variant="soft" size="1">New</Badge>
{/* Solid with accent colour closest to "success" intent */}<Badge variant="solid" color="green" size="2">Active</Badge>
{/* High-contrast surface badge */}<Badge variant="surface" highContrast>Beta</Badge>
{/* Custom radius */}<Badge variant="soft" radius="small">Tag</Badge>
{/* asChild — render badge markup on a consumer element */}<Badge asChild variant="soft"> <mark>Highlight</mark></Badge>Divergence
| From | Type | → To | Rationale |
|---|---|---|---|
anatomy[icon-leading] | omitted | — | Radix Themes Badge has no icon-leading named slot or prop. Any icon placed before badge text is a free React child; the component applies gap via CSS automatically with no mechanism to mark a child as "leading icon" versus arbitrary preceding content. This is the same pattern as Radix Themes Button. (https://www.radix-ui.com/themes/docs/components/badge) |
anatomy[content] | omitted | — | Radix Themes Badge has no named content slot. All badge text is a free React child rendered inside the `<span>` root. The canonical content slot exists to pin the live-region contract (aria-live on that slot for count updates); Radix has no equivalent structural anchor — the entire badge `<span>` is the only element and no aria-live wiring is applied by the library. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[default] | reshaped | variant="soft" (default) — fill-style vocabulary, no severity mapping | Radix Themes Badge models fill style, not semantic severity. The four variants are `solid`, `soft` (default), `surface`, and `outline` — each controls the fill treatment independently of any semantic intent. The canonical `default` severity maps loosely to `soft` in neutral colour, but Radix provides no first-class severity vocabulary. A design system using Radix must compose (variant × color) to encode severity: e.g. `variant="soft" color="green"` for success intent. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[success] | omitted | — | No `success` variant. Radix's fill-style taxonomy has no semantic severity tier. Success intent is achieved by a consumer-side convention: `color="green"` combined with any fill variant. The canonical closed enum (default / success / warning / error / info) does not exist as a prop axis; severity is expressed through the orthogonal `color` prop. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[warning] | omitted | — | No `warning` variant. Warning intent maps to `color="amber"` or `color="orange"` combined with a fill variant; there is no first-class warning semantic. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[error] | omitted | — | No `error` (danger) variant. Error intent maps to `color="red"` or `color="crimson"` combined with a fill variant. Radix does not encode semantic severity in any component prop. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[info] | omitted | — | No `info` variant. Info intent maps to `color="blue"` or `color="indigo"` combined with a fill variant. Severity is entirely a consumer convention on top of Radix's (variant × color) matrix. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.properties[size] | reshaped | size="1" | "2" | "3" (numeric, responsive, default "1") | Canonical uses semantic names (sm / md). Radix Themes Badge uses a numeric t-shirt scale 1–3 where "1" is smallest. The scale also supports Responsive<> wrapping so each breakpoint can carry a different numeric tier — a capability the canonical sm/md enum does not model. No mechanical mapping exists; consumers must decide whether their canonical sm maps to "1" or "2". (https://www.radix-ui.com/themes/docs/components/badge) |
axes.properties[dot] | omitted | — | Radix Themes Badge has no `dot` boolean. There is no built-in collapsed dot-indicator variant. Consumers needing a dot badge must render a custom element; Radix does not ship the css treatment, the aria-label contract, or the radius-full override that the canonical dot variant specifies. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.properties[hasIcon] | omitted | — | Radix Themes Badge has no `hasIcon` prop. Whether an icon appears is determined by whether the consumer includes an icon element as a child; there is no declarative prop controlling icon presence or the gap treatment between icon and label. (https://www.radix-ui.com/themes/docs/components/badge) |
events[countChange] | omitted | — | Radix Themes Badge is a purely static display component; it ships no events and no controlled count prop. The canonical countChange event is predicated on the Badge acting as a live counter. Consumers that need live-count behaviour must implement the aria-live wrapper, the count prop, and the change event themselves — nothing in Radix Themes Badge assists with this. (https://www.radix-ui.com/themes/docs/components/badge) |
motion.durations | omitted | — | Radix Themes Badge ships no motion durations. There are no enter/exit animations, no countTick animation, and no severityChange transition. The canonical motion.durations.countTick and severityChange token bindings have no equivalent surface in the library. (https://www.radix-ui.com/themes/docs/components/badge) |
motion.easing | omitted | — | Radix Themes Badge applies no configurable easing. Motion is not a first-class concern for a static display component in Radix Themes; no easing token is exposed. (https://www.radix-ui.com/themes/docs/components/badge) |
anatomy[root] | extended | + `color` prop (AccentColor enum — indigo, cyan, orange, crimson, red, amber, green, blue, gray, …), `highContrast` (boolean), `radius` ("none" | "small" | "medium" | "large" | "full"), `asChild` (boolean). These four props have no canonical counterparts. | Radix Themes Badge exposes the same palette of composability props as every other Radix Themes component. `color` decouples accent colour from fill variant, enabling (e.g.) a soft green badge or a solid crimson badge without a severity variant. `highContrast` boosts foreground/ background contrast for accessibility-critical or emphasis contexts. `radius` allows per-instance corner radius override of the theme default. `asChild` (via Radix Slot) renders badge behaviour on a consumer-supplied element. These are Radix Themes architectural conventions applied consistently across the component library, not Badge-specific choices. (https://www.radix-ui.com/themes/docs/components/badge) |
axes.variants[default] | extended | + variant="solid" | "surface" | "outline" — three additional fill-style tiers beyond the canonical severity vocabulary. Combined with the full AccentColor enum, Radix Themes exposes a (4 variants × ~30 colours) matrix that the canonical five-tier severity enum does not model. | Radix Themes separates fill style from semantic severity, giving consumers explicit control over visual weight (solid → heaviest, outline → lightest) independently of colour intent. A design system author layers a severity convention on top of this matrix rather than receiving it as first-class library behaviour. This is a deliberate product decision to keep the primitive flexible and design-system- agnostic, at the cost of requiring the consumer to encode severity semantics themselves. (https://www.radix-ui.com/themes/docs/components/badge) |
Why this audit reads the way it does
Radix Themes Badge is a styled, opinionated display primitive derived from Radix Themes' design system. Unlike Radix Primitives, which are unstyled and behaviour-focused, Radix Themes Badge ships a full visual treatment (4 fill variants, 3 size tiers, ~30 accent colours, radius control) but deliberately stops short of semantic severity vocabulary, slot anatomy, motion, and live-region contracts. The three most substantive divergences are: 1. Variant taxonomy: Radix models fill style (solid / soft / surface / outline), not semantic severity (default / success / warning / error / info). The entire canonical severity ladder is absent; severity is a consumer convention achieved through the (variant × color) matrix. 2. No named sub-slots: icon-leading and content are free children. The canonical slot contract — which pins the aria-live live-region anchor to the content slot and marks icon-leading as decorative — has no structural equivalent in Radix Themes Badge. 3. No live-count support: Radix Themes Badge is purely static. It ships no controlled count prop, no aria-live wiring, no countChange event, and no dot variant. All of the canonical count and dot mechanics are consumer responsibilities. The `color`, `highContrast`, `radius`, and `asChild` props are Radix Themes architectural conventions that give consumers compositional power exceeding the canonical surface.
Figma↔Code mismatches
Where designer and developer worlds typically misalign on this component.
- 01 Figma
Severity drawn as a single static colour swatch per variant
CodeA `data-severity` attribute or a `variant` prop driving CSS modifiers
ConsequenceDesigners ship five separate frame-variants; developers map severity to a single prop, then translate the prop to colour tokens. The variant matrix doubles unnecessarily and severity drift becomes invisible (`success` and `info` may share a hue in some implementations without anyone noticing).
CorrectModel severity as a single `variant` prop with closed-enum values (default, success, warning, error, info). The CSS contract picks tokens per severity; the Figma component property mirrors the same closed enum.
- 02 Figma
Dot variant drawn as a separate component
CodeA boolean `dot` property toggling the content slot off
ConsequenceDesigners create a "Dot Badge" alongside "Badge" and the two drift independently — different sizes, different padding, different border treatments. The single canonical anatomy forks visually.
CorrectModel `dot` as a property of the same Badge component. When `dot: true` the content slot collapses, the root rect renders as a small circle (radius.full), and the accessible name moves from content to an `aria-label` on root.
- 03 Figma
Count baked as a static text in the Figma component
CodeA controlled prop receives the count and renders it inside the content slot
ConsequenceDesigners update the Figma badge with one fixed number ("3" or "12"). Developers shipping the live count have no design reference for the "99+" overflow pattern, the zero-count- hidden behaviour, or the live-region announcement.
CorrectDocument `count: number | null` as the canonical input, the "99+" overflow shape, and the `aria-live="polite"` live-region contract. The Figma file documents these states as variants of the canonical Badge, not as separate components.
- 04 Figma
Pill shape drawn as fixed-radius frame
CodeBorder-radius driven by `radius.pill` token
ConsequenceDesigners hard-code a px radius (8, 12, 999); developers map to `radius.pill`. When the token-system rebalances to `radius.full` (true circular), the Figma file lags and the shipped badge looks different from the design.
CorrectBind the radius to a token in both surfaces. Document `radius.pill` as the canonical Badge radius; rect-style badges (status flags) use `radius.sm` and live as a separate variant rather than a per-instance override.
Variants, properties, states
Variants
Structurally different versions of the component.
default success warning aka caution · attention error aka danger · destructive · critical info Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md |
dot | boolean |
hasIcon | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | |
data | idleupdatedmaxhidden |
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps default / success / warning / error / info severity grade. |
Size | Enum | size | Maps sm / md visual size. |
Dot | Boolean | dot | When true, content slot collapses and root renders as a small circle. |
Has Leading Icon | Boolean | hasIcon | — |
Leading Icon | Slot | icon-leading | Hosts an Icon instance; decorative by default with `aria-hidden`. |
Label | Text | label | Badge text content. For numeric counts use the controlled `count` prop instead. (Slot id renamed `content` → `label` per P6-150 / ADR-034 — converged with `icon-leading-text` sub-anatomy.) |
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Auto-layout horizontal pill; padding from size token, radius from variant |
icon-leading
from icon-leading-text | instance | Icon component instance; size bound to host's size token |
label
from icon-leading-text | text | Text style bound to a "label" component property; truncates with ellipsis when single-line variant overflows |
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
root | root | span-or-mark |
icon-leading
from icon-leading-text | icon-leading | presentational-or-img |
label
from icon-leading-text | label | text |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-badge>` host element with named slots (`<slot name="icon">`, default slot for content); attribute-driven variants and properties | attributes (`variant="success"`, `size="md"`, `dot`); `data-state` for live-update animation states |
| React | Function component wrapping a `<span>` (or `<mark>` when semantically appropriate) with conditional icon and content slots; libraries include Radix Themes Badge, Polaris Badge, Atlassian Lozenge / Badge | props with class-variance-authority for variant / size; controlled `count`-prop for live values; Material UI exposes a `<Badge>` wrapper that anchors its child |
| Angular (signals) | Angular component with `<ng-content>` for label, `<ng-content select="[badge-icon]">` for icon, signal-based input for count | input<'default' | 'success' | 'warning' | 'error' | 'info'>(); host-bindings drive `[attr.role]` and `[attr.aria-live]` |
| Vue | Single-file component with `<slot name="icon">` and default slot; v-bind for severity prop; PrimeVue Badge / Naive UI Badge as third-party precedents | defineProps with literal-union types for severity; v-model:count for controlled live updates |
Events
countChangeoptional
Internationalisation
RTL · mirroring
Badge layout is direction-neutral when the content is digits or short single words. Icon-leading slot anchors to the logical-start edge (left in LTR, right in RTL) via `inset-inline-start`. Counts ("12", "99+") read identically in either direction; SR-announcement order stays icon-then-content.
Text expansion
Severity-word badges ("Beta", "New", "Pending") expand 30–50 % in German and Romance languages. Reserve padding using `min-width` rather than fixed `width`; allow the badge to grow horizontally rather than truncate. Counts do not expand.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
root | Root carries the badge's accessible name when content alone does not (icon-only or dot variants). For changing counts, set `aria-live="polite"` on the root or on the content slot. For decorative dot variants pair `role="presentation"` with a sibling visually-hidden text label. | |
icon-leading | `aria-hidden="true"` on the SVG when the icon is purely decorative (paired with a visible label). If the icon is the sole signal of the host's intent (icon-only host variant), the host carries an `aria-label` describing the action — the icon never announces itself. | |
label | Plain text node; no special role. The label is the host's accessible name unless overridden by `aria-label` / `aria-labelledby`. Avoid visually-hidden modifiers — the visible text and the announced name should match. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab (badge inside a focusable host) | Focus enters the host element (button, link, list-item). Badge is not in the tab order — it is presentational metadata about the host. Subsequent Tab leaves the host; the badge is not visited as a separate stop. |
Tab (interactive badge, rare) | When the consumer promotes Badge to interactive (uncommon), the badge participates in the tab order as a single stop and Enter activates it. Hit target ≥ 24×24 px per WCAG 2.5.8. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| SR enters a host with a badge child | SR reads the host's accessible name followed by the badge's accessible name in DOM order. Decorative dot badges with `role="presentation"` are skipped; iconic badges with `aria-label` read their label. |
| Live count updates | SR announces the new count value via the polite live region. Throttled by the implementation to avoid flooding SR output during rapid increments. |
axe-core rules to assert
aria-allowed-attraria-valid-attr-valuecolor-contrastrole-img-alt
Same data as JSON for direct ingestion into Playwright + @axe-core/playwright or Jest + jest-axe:
/api/components/badge/a11y-fixture.json
Contracts
Non-negotiable contracts
WCAGWCAG 1.4.1 — Use of Color Severity is paired with a non-colour cue — an icon, a textual prefix in the content, or a visually-hidden severity word in the accessible name. Colour alone is reinforcement, not the contract.
Colour-only severity excludes users with colour-vision deficiencies and SR users entirely. The variant axis is a structural commitment; the visual treatment must mirror it in at least two channels.
APGAPG: Live Region patterns — polite vs assertive Live counts wrap in `aria-live="polite"` on the root or content slot, with `aria-atomic="true"` so the full new value reads. Throttle SR-announcements at the implementation layer to avoid flooding under rapid increments.
Without the live region, count updates are sighted-only — SR users hear the new value only on re-navigation to the badge, after the underlying state-change has lost relevance. Counts that change without announcement misrepresent the surface to SR users.
APGAPG: presentation role + visually-hidden text patterns Decorative-only badges (dot variant or icon-only without meaningful content) carry `role="presentation"` plus a sibling visually-hidden text label in the host's accessible name. The badge's meaning lives in the label, not in the visual.
Without the host-level label, SR users encounter an unannotated dot or icon and lose the metadata the badge was supposed to carry. The decoration is sighted-only and the dot becomes invisible to AT.
Vocabulary drift
- Polaris
Badge- Carbon
Tag- Carbon labels its Badge-equivalent "Tag", which collides with the Tag Input pattern in this canon. Implementation audits document the per-library naming via `componentName`.
- Material 3
Badge- Atlassian
Lozenge- Atlassian's "Lozenge" is the closest Badge-equivalent; their "Badge" term is reserved for notification-count markers anchored to icons.
- Radix
Badge
Common mistakes
#badge-color-only-meaning
Severity conveyed by colour alone
The badge variant is communicated only by colour (red for error, yellow for warning). Users with colour-vision deficiencies cannot distinguish severity; SR users hear the content but not the severity grade.
Pair colour with a non-colour cue — an icon-leading glyph (check, exclamation, info), a textual prefix in the content ("Error: …"), or a visually-hidden severity word in the accessible name. Colour is reinforcement, not the contract.
#badge-target-too-small
Interactive badge with sub-24px target
Some products promote Badge to interactive (badge-as-tag, badge-as-filter). When the badge is the activator, the hit target falls below WCAG 2.5.8 minimum 24×24 px (44×44 AAA), excluding motor-impaired and touch users.
When the canonical Badge is interactive, the consumer should reach for `Tag Input` or `Button` instead — both ship the hit-target contract. If the design genuinely needs an interactive badge, the host extends the badge with hit-area padding (CSS `::after` overlay) so the visible badge stays compact while the activation region meets the threshold.
#badge-count-no-aria-live
Live counts update silently
Notification counts increment as messages arrive, but the badge has no `aria-live` wrapper. Sighted users see the number tick; SR users hear nothing until they re-navigate to the badge.
Set `aria-live="polite"` on the root or content slot of any badge whose value updates without page navigation. Use `aria-atomic="true"` so the full new value reads, not just the delta. Throttle announcements (debounce) for fast-moving counts to avoid SR flooding.