# Badge

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

- Status: stable
- Group: display
- Page: https://www.digitaltableteur.com/design-system/components/badge
- Contract: https://www.digitaltableteur.com/contracts/v1/Badge.contract.json

## 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` (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.

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

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Badge.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/badge.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/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.
