Designer 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).

Highlight
Fig 1.1 · Badge · Designer view

Implementations

How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.

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

Designer

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
Designer

Token usage per slot

root
spacing
  • paddingspacing.tight
radius
  • cornerradius.pill
color
  • backgroundcolor.surface.raised
  • foregroundcolor.text.primary
  • bordercolor.border.subtle
typography
  • sizetext.xs
  • weightweight.semibold
icon-leading
spacing
  • gapspacing.compact
color
  • foregroundcolor.text.muted
label
color
  • foregroundcolor.text.primary
typography
  • sizetext.xs
  • weightweight.semibold
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps default / success / warning / error / info severity grade.
SizeEnumsizeMaps sm / md visual size.
DotBooleandotWhen true, content slot collapses and root renders as a small circle.
Has Leading IconBooleanhasIcon
Leading IconSloticon-leadingHosts an Icon instance; decorative by default with `aria-hidden`.
LabelTextlabelBadge 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.)
Designer

Motion

TransitionDuration token
countTickmotion.duration.fast
severityChangemotion.duration.fast
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smBelow this width, badges hosted inside list-rows or nav- items collapse the content slot and render as a dot variant. The full count moves to `aria-label` so SR users retain the value while the visible footprint shrinks.
Both

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.

Both

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.

PropertyType
size sm | md
dot boolean
hasIcon boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
data
idleupdatedmaxhidden
Both

Figma↔Code mismatches

  1. 01
    Figma

    Severity drawn as a single static colour swatch per variant

    Code

    A `data-severity` attribute or a `variant` prop driving CSS modifiers

    Consequence

    Designers 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).

    Correct

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

  2. 02
    Figma

    Dot variant drawn as a separate component

    Code

    A boolean `dot` property toggling the content slot off

    Consequence

    Designers create a "Dot Badge" alongside "Badge" and the two drift independently — different sizes, different padding, different border treatments. The single canonical anatomy forks visually.

    Correct

    Model `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.

  3. 03
    Figma

    Count baked as a static text in the Figma component

    Code

    A controlled prop receives the count and renders it inside the content slot

    Consequence

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

    Correct

    Document `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.

  4. 04
    Figma

    Pill shape drawn as fixed-radius frame

    Code

    Border-radius driven by `radius.pill` token

    Consequence

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

    Correct

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

Both

Contracts

Non-negotiable contracts

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

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

  3. 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
Designer

Common mistakes

Blocker

#badge-color-only-meaning

Severity conveyed by colour alone

Problem

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.

Fix

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.

Blocker

#badge-target-too-small

Interactive badge with sub-24px target

Problem

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.

Fix

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.

Major

#badge-count-no-aria-live

Live counts update silently

Problem

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.

Fix

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.

Major

#badge-icon-decorative-not-hidden

Icon-leading slot announced as duplicate of label

Problem

The leading icon (check, exclamation) carries no `aria-hidden`; SR reads "checkmark, success" or "exclamation-mark, warning" with the severity word echoing the icon meaning. Verbose announcement, no added information.

Fix

Decorative icons paired with a visible label set `aria-hidden="true"`. Only when the icon is the entire badge content (dot or icon-only variant) does it carry the accessible name — moved to the badge root via `aria-label`, not onto the icon itself.

Accessibility hints
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.