Skip to main content

Design system components

Badge

Compact status label with an orthogonal variant (visual weight) and tone (semantic colour), a size, and an optional icon.

Status: stable. Group: display. Markdown version: /design-system/components/badge.md

When to use

Badge carries a short piece of meaning, a state, a count, or a tag, in a compact pill. Two orthogonal axes encode the look: variant for visual weight (primary filled, secondary outlined) and tone for semantic color. A matching status icon is auto-resolved for non-neutral tones, and removable turns the badge into a dismissible filter chip.

When not to use

  • Do not rely on colour alone to convey tone; pair it with the icon or include the state word in the text.
  • Do not nest a Button inside a Badge beyond the built-in removable affordance.
  • Do not use Badge as an interactive action or toggle; use Button for actions and Switch or Checkbox for state.

Look-alikes

  • Button: Do not use Badge as an interactive action or toggle; use Button for actions and Switch or Checkbox for state.
  • Card: Do not use the muted variant to encode status; status belongs to AlertBanner or Badge.
  • Checkbox: Do not use Badge as an interactive action or toggle; use Button for actions and Switch or Checkbox for state.
  • StatusDot: Do not use StatusDot for counts, filters, or removable chips; Badge owns those surfaces.
  • Switch: Do not use Badge as an interactive action or toggle; use Button for actions and Switch or Checkbox for state.

Props

Badge props
PropTypeDefaultDescriptionRequired
childrenReact.ReactNodenoneLabel. Rendered exactly as given (never looked up as a translation key), so pass already translated text.Yes
variant"primary" | "secondary"primaryVisual weight. Defaults to primary.No
tone"info" | "success" | "warning" | "error" | "neutral"neutralSemantic colour, orthogonal to variant. When omitted no tone class is applied and no semantic icon is added.No
size"sm" | "md" | "lg" | "xs"mdSize. Defaults to md.No
classNamestringnoneNo description in the contract.No
removablebooleanfalseRenders a dismiss control that removes the badge. On dismiss, focus moves to the next focusable element (else the previous one) before the badge unmounts. Defaults to false.No
onRemove() => voidnoneCalled after the badge is dismissed and focus has moved; focus it sets here wins.No
iconReact.ReactNodenoneLeading icon; a semantic icon is supplied automatically for non-neutral tones.No
dotbooleanfalseRender a leading StatusDot instead of the semantic icon, for example on lifecycle badges (alpha/beta/stable/deprecated). The dot paints the badge's own text colour (currentcolor), so on a filled badge it matches the label rather than the tone. An explicit icon still wins.No
squarebooleanfalseSquare (non-pill) corners.No
role"status"noneARIA role. "status" makes dynamic updates announce via aria-live="polite" (e.g. a live notification count). Omit for static decorative badges.No
hrefstringnoneMakes the badge a link, for example a tag that opens a filtered list. It renders through the injected link component (the host's router link, else a plain anchor). A linked badge cannot also be removable.No

Variants

  • Available variants: primary, secondary (default primary).
  • Available tones: neutral, error, warning, success, info (default neutral).
  • Available sizes: xs, sm, md, lg (default md).

Invalid combinations

  • MUST NOT: When href is set and removable is set, then do not set removable. A linked badge is one link; a remove button inside it would nest an interactive control in a link. Use a plain removable badge, or a linked badge without removable.

Keyboard

The contract records no keyboard interaction.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

Badge rules
LevelRuleHow it is checked
MUST NOTWhen href is set and removable is set, then do not set removable. A linked badge is one link; a remove button inside it would nest an interactive control in a link. Use a plain removable badge, or a linked badge without removable.Checked by the MCP tool validate_component_usage (rule badge-link-not-removable). Also enforced in code: Badge.tsx development warning (href && removable); the link branch renders no remove button.
MUSTARIA: Meaningful text or aria-label when icon-only.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Do not rely on colour alone; pair tone with the icon.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Removable: the dismiss control is a native button with a translated aria-label; on dismiss focus moves to the next focusable element after the badge (else the previous one) before it unmounts, and focus set in onRemove wins.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDUse tone to encode state (success, warning, error, info) and keep the label to a word or two.Not stated in the contract.
SHOULDUse removable with onRemove for filter chips; the built-in dismiss button ships with a translated accessible name.Not stated in the contract.
SHOULDUse role=status only for live counters that update at runtime; aria-live=polite announces changes without moving focus.Not stated in the contract.
SHOULDUse variant=secondary (outlined) when a filled badge would compete with nearby primary actions.Not stated in the contract.
SHOULD NOTDo not put role=status on a static badge; live-region churn is wasted when the content never changes.Not stated in the contract.
SHOULD NOTDo not encode meaning with tone alone; the label must carry the meaning for color-blind users.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { Badge } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/identity exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-badge>; see the custom elements manifest.