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
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
children | React.ReactNode | none | Label. Rendered exactly as given (never looked up as a translation key), so pass already translated text. | Yes |
variant | "primary" | "secondary" | primary | Visual weight. Defaults to primary. | No |
tone | "info" | "success" | "warning" | "error" | "neutral" | neutral | Semantic colour, orthogonal to variant. When omitted no tone class is applied and no semantic icon is added. | No |
size | "sm" | "md" | "lg" | "xs" | md | Size. Defaults to md. | No |
className | string | none | No description in the contract. | No |
removable | boolean | false | Renders 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 | () => void | none | Called after the badge is dismissed and focus has moved; focus it sets here wins. | No |
icon | React.ReactNode | none | Leading icon; a semantic icon is supplied automatically for non-neutral tones. | No |
dot | boolean | false | Render 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 |
square | boolean | false | Square (non-pill) corners. | No |
role | "status" | none | ARIA role. "status" makes dynamic updates announce via aria-live="polite" (e.g. a live notification count). Omit for static decorative badges. | No |
href | string | none | Makes 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(defaultprimary). - Available tones:
neutral,error,warning,success,info(defaultneutral). - Available sizes:
xs,sm,md,lg(defaultmd).
Invalid combinations
- MUST NOT: When
hrefis set andremovableis set, then do not setremovable. 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.
| Level | Rule | How it is checked |
|---|---|---|
| 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. | 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. |
| MUST | ARIA: Meaningful text or aria-label when icon-only. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Do not rely on colour alone; pair tone with the icon. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: 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. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The 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. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Use tone to encode state (success, warning, error, info) and keep the label to a word or two. | Not stated in the contract. |
| SHOULD | Use removable with onRemove for filter chips; the built-in dismiss button ships with a translated accessible name. | Not stated in the contract. |
| SHOULD | Use role=status only for live counters that update at runtime; aria-live=polite announces changes without moving focus. | Not stated in the contract. |
| SHOULD | Use variant=secondary (outlined) when a filled badge would compete with nearby primary actions. | Not stated in the contract. |
| SHOULD NOT | Do not put role=status on a static badge; live-region churn is wasted when the content never changes. | Not stated in the contract. |
| SHOULD NOT | Do 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/identityexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-badge>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.