---
title: "Badge"
description: "Marks an icon, a navigation item or an avatar with a dot or a small count to show that something is waiting."
url: "https://tanrei.chnotchy.com/components/badge/"
last_updated: 2026-10-26T22:31:46.000Z
---

# Badge

> Marks an icon, a navigation item or an avatar with a dot or a small count to show that something is waiting.

## Overview

### Usage

**Web**

```html
<!-- No count: the dot. A dot needs a label. -->
<tanrei-badge label="New messages">
  <tanrei-button variant="text" label="Chat"><span slot="icon">chat</span></tanrei-button>
</tanrei-badge>

<!-- 0 draws nothing; a count above max (999) shows as 999+. -->
<tanrei-badge count="3" label="3 unread">
  <tanrei-button variant="text" label="Inbox"><span slot="icon">inbox</span></tanrei-button>
</tanrei-badge>

<script type="module">
  import '@tanrei/web/components/badge';
  import '@tanrei/web/components/button';
  const badge = document.querySelector('tanrei-badge[count]');
  badge.count = unread;
  badge.label = `${unread} unread`;
</script>
```

**React**

```tsx
import { Badge, Button } from '@tanrei/react';

<Badge label="New messages">
  <Button variant="text" label="Chat" onClick={openChat}><span slot="icon">chat</span></Button>
</Badge>
<Badge count={unread} label={`${unread} unread`}>
  <Button variant="text" label="Inbox" onClick={openInbox}><span slot="icon">inbox</span></Button>
</Badge>
```

**SwiftUI**

```swift
import Tanrei

// nil is the dot.
TanreiBadge(count: nil, label: "New messages") {
  TanreiIconButton("Chat", action: openChat) { Image(systemName: "bubble.left") }
}
TanreiBadge(count: unread, label: "\(unread) unread") {
  TanreiIconButton("Inbox", action: openInbox) { Image(systemName: "tray") }
}

// The same as a modifier on any view.
Image(systemName: "bell").accessibilityLabel("Notifications").tanreiBadge(1200, label: "More than 999 unread")
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiBadge
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.primitives.TanreiText

// null is the dot.
TanreiBadge(count = null, contentDescription = "New messages") {
    TanreiIconButton(onClick = ::openChat, contentDescription = "Chat") { TanreiText("chat") }
}
TanreiBadge(count = unread, contentDescription = "$unread unread") {
    TanreiIconButton(onClick = ::openInbox, contentDescription = "Inbox") { TanreiText("inbox") }
}
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

// No count: the dot.
TanreiBadge(
  semanticLabel: 'New messages',
  child: TanreiIconButton(onPressed: openChat, semanticLabel: 'Chat', icon: const Icon(AppSymbols.chat)),
);
TanreiBadge(
  count: unread,
  semanticLabel: '$unread unread',
  child: TanreiIconButton(onPressed: openInbox, semanticLabel: 'Inbox', icon: const Icon(AppSymbols.inbox)),
);
```

**React Native**

```tsx
import { Badge, IconButton } from '@tanrei/react-native';

<Badge label="New messages">
  <IconButton accessibilityLabel="Chat" onPress={openChat}>chat</IconButton>
</Badge>
<Badge count={unread} label={`${unread} unread`}>
  <IconButton accessibilityLabel="Inbox" onPress={openInbox}>inbox</IconButton>
</Badge>
```

## Specs

- Category: Communication
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/badge/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/badge.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-badge>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/badge`).
- React: `Badge` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-badge` | lagging |
| SwiftUI | `TanreiBadge` | lagging |
| Jetpack Compose | `TanreiBadge` | lagging |
| Flutter | `TanreiBadge` | lagging |
| React Native | `Badge` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticLabel` | `label` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `container`: The pill that holds the count: at least as wide as it is high, so a single digit sits in a circle.
- `dot` (optional): The container's form when there is no count: a small circle with no label.
- `label` (optional): The count, or the capped count followed by a plus sign.
- `outline`: A ring outside the dot or the pill that separates the badge from what it overlaps.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `count` | `number` | — | `count` | How many things are waiting. Null draws the dot (something is new, the number is unknown or unimportant); 0 draws nothing; above 0 draws the number. |
| `max` | `number` | `999` | `max` | The largest count shown as it is; a larger one shows as max followed by a plus sign (999+). |
| `label` | `string` | `""` | `label` | What assistive technology reads after the anchor's own name ("3 unread"). With no label, the displayed count is read; a dot needs one. |

### Slots

| Slot | Description |
| --- | --- |
| `default` | The anchor the badge sits on, which it needs: an icon, an icon-only Button, a navigation item or an avatar. Its first element is the anchor; a badge with no element to sit on draws nothing (BDG-16). In fixtures the anchor is the glyph of the system's icon set that the fixture's content names, drawn at the icon size sys.icon.medium in the color on-surface-variant; its accessible name is that glyph name ("mail"), and the badge adds the fixture's label to it as its value ("mail, 3 unread"). |

### States

`enabled`

### Accessibility

- Role: `none`
- Labeling: The badge has no role or name of its own. label (or, without one, the displayed count) becomes part of the anchor's accessible description, so the anchor is announced as, for example, "Inbox, button, 3 unread". The drawn number is hidden, so it is never read twice.
- Minimum target size: yes

- Not interactive: the badge takes no input (BDG-7), so the minimum target is the anchor's and the badge never shrinks or covers it for input.
- A dot conveys its meaning by color and position only, so a dot without a label fails for screen reader users (BDG-9); give it one ("new messages").
- Where the count must be announced when it changes (a live chat), the product uses a live region; the badge does not announce by itself (BDG-10).
- A badge marks an anchor and never stands alone in a line or a row (BDG-16). A count that stands by itself, such as the count at the end of a navigation drawer's row, a tree view's row or a menu item, is text, read in its place as the row's own words: that component's trailing text. A word that names a state is a tag.
- How the description reaches the anchor is each platform's idiom: the web sets it on the slotted anchor (an ID reference cannot cross the shadow root), Apple platforms append it to the anchor's accessibility value, Compose merges it into the anchor's semantics as a state description, Flutter adds it to the anchor's semantics value, React Native to its accessibility value.

### Behaviour

Requirements in EARS form, each with its stable id.

- **BDG-1** WHILE count is null THE badge SHALL draw a circle of diameter comp.badge.dot.size in comp.badge.container.color with no label, its center comp.badge.dot.offset inside the anchor's top-end corner on both axes; the dot is a circle whatever corners the container has.
- **BDG-2** WHILE count is above 0 THE badge SHALL draw a container at least comp.badge.container.height high and at least as wide as it is high, with comp.badge.container.padding-inline on each side of its label and corners of comp.badge.container.shape, filled with comp.badge.container.color, and the label centered in comp.badge.label.typescale and comp.badge.label.color.
- **BDG-3** WHILE count is above 0 THE badge SHALL place the center of the container's leading circle (a square of the container's drawn height at its start) comp.badge.container.offset inside the anchor's top-end corner on both axes, so that a longer count grows toward the end, away from the anchor.
- **BDG-4** WHEN count is greater than max THE badge SHALL show max followed by a plus sign (999+ with the default max) instead of count, and SHALL otherwise show count in full.
- **BDG-5** WHILE count is 0 THE badge SHALL draw nothing and expose nothing to assistive technology, and its anchor SHALL render and be announced as it would without a badge.
- **BDG-6** THE badge SHALL draw comp.badge.outline.width of comp.badge.outline.color around the dot or the container, outside its edge, without changing the badge's size or position.
- **BDG-7** THE badge SHALL NOT change its anchor's size or position, SHALL NOT take space in the layout (it may extend past the anchor's bounds), and SHALL NOT take pointer, touch or keyboard input: input over the badge reaches the anchor.
- **BDG-8** THE badge SHALL hide its drawn dot and number from assistive technology and SHALL add label to its anchor's accessible description, so that the anchor is announced with its own name followed by label; WHILE label is empty and count is above 0 THE badge SHALL add the displayed text instead (for example 999+).
- **BDG-9** WHILE count is null and label is empty THE badge SHALL still render, and in debug builds report that the dot tells assistive technology nothing.
- **BDG-10** WHEN count or label changes THE badge SHALL update its anchor's accessible description and SHALL NOT announce the change by itself.
- **BDG-11** WHERE the layout direction is right-to-left THE badge SHALL take the anchor's top-left corner as its top-end corner and grow toward the left, and SHALL isolate its label from the surrounding direction so that a capped count keeps its plus sign after the digits.
- **BDG-12** THE badge SHALL format count and max with the current locale's digits and no grouping separator, and SHALL draw digits with tabular figures where the font has them, so that a changing count does not shift its neighbours.
- **BDG-13** WHILE the text size is enlarged THE badge SHALL grow its container with its label, keep the center of the leading circle — a square of the grown container's height — at its placement, and keep the dot at comp.badge.dot.size.
- **BDG-14** IF count is negative or not a number THE badge SHALL render as for 0; IF count is not a whole number THE badge SHALL use it rounded down; IF max is below 1 THE badge SHALL use its default of 999; in each case debug builds SHALL report the value.
- **BDG-15** THE badge SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the badge is shown THE badge SHALL draw itself for the new direction at once, without a motion of its own, keeping what it shows and its state.
- **BDG-16** IF the badge has no anchor THE badge SHALL draw nothing, take no space and expose nothing to assistive technology, whatever its count and label, and in debug builds report that a badge needs an anchor; WHEN an anchor is given to it THE badge SHALL draw itself on that anchor (BDG-1, BDG-3) and describe it (BDG-8) at once, and WHEN its anchor is taken away it SHALL give the anchor back its own description and draw nothing again.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.badge.…` → `--tanrei-comp-badge-…`).

#### `tanrei.comp.badge`

| Token | Type | Value |
| --- | --- | --- |
| `comp.badge.dot.size` | `dimension` | `6px` |
| `comp.badge.dot.offset` | `dimension` | `{tanrei.sys.space.space25}` |
| `comp.badge.container.height` | `dimension` | `16px` |
| `comp.badge.container.padding-inline` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.badge.container.offset` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.badge.container.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.badge.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.badge.label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.badge.label.typescale` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.badge.outline.color` | `color` | `{tanrei.sys.color.surface}` |
| `comp.badge.outline.width` | `dimension` | `{tanrei.sys.border.thick}` |

## Guidelines

### When to use

Use a badge to show that something is waiting behind an icon, a navigation item or an avatar: unread messages, new activity, updates to install. It draws the eye to where the person can act on it. A badge has no word and no place of its own in the layout, so it is not a status label. For a word that names a status or a category, such as “New”, “Beta” or “Pro”, use a [tag](https://tanrei.chnotchy.com/components/tag/).

### Badge, chip or tag

The three are told apart by what they do:

| Kind | What it is | Where it stands | Takes input |
| --- | --- | --- | --- |
| Badge | A dot or a count attached to another element. | On its anchor’s corner, outside the layout. | No. Input reaches the anchor. |
| [Chip](https://tanrei.chnotchy.com/components/chip/) | A control. | In the layout. | Yes: it is pressed, switched or removed, and has focus and states. |
| [Tag](https://tanrei.chnotchy.com/components/tag/) | A static label. | In the layout, inline. | Never. |

1. If it is a number or a dot on top of something else, it is a badge.
2. If it is pressed, selected or removed, it is a chip. If it leads somewhere, it is a link.
3. If it is a word that only says what a thing is or what state it is in, it is a tag.

A count that stands by itself in a row, such as the number of results beside a heading, is not a badge either: a badge always has an anchor, and one with nothing to sit on draws nothing. Write such a count as text, read in its place with the row’s other words: at the end of a [navigation drawer](https://tanrei.chnotchy.com/components/navigation-drawer/) item it is the item’s `trailingText`. Where it names a state, use a tag.

### Choosing a form

The form follows `count`, so there is no variant to choose.

| Form | `count` | Use for |
| --- | --- | --- |
| Dot | none (null) | Something is new, and the number is unknown or does not matter. |
| Count | above 0 | The number helps the person decide whether to look now: unread messages, pending requests. |
| Nothing | 0 | Nothing is waiting. Bind the badge to the count directly; you do not need a condition around it. |

A count above `max` shows as `max+` (999+ by default), so the badge never grows past four characters. Lower `max` (to 99, for example) where the anchor is small or the exact number stops mattering sooner.

### Do

- Give every badge a label that reads well after the anchor’s name: “3 unread” after “Inbox”. A dot needs one most of all, because without it the dot says nothing to a screen reader.
- Leave room around the anchor. The badge sits on its top-end corner and may extend past its bounds; it never moves or resizes the anchor.
- Clear the badge when the person has seen what it points to, so it keeps meaning something.

### Don’t

- Don’t badge every item. When everything is marked, nothing stands out.
- Don’t put a badge on a button’s label or in running text. It belongs on an icon, a navigation item or an avatar. A word beside a label or in a line of text is a tag.
- Don’t use the badge’s color to say what kind of thing is waiting, or how urgent it is. Say it in the label, or in the content it points to.
- Don’t make the badge interactive. Taps and clicks on it reach the anchor, which is where the action is.

## Accessibility

### What the component handles

- The drawn dot and number are hidden from assistive technology, so the count is never read twice.
- The label (or, without one, the displayed count such as “999+”) becomes part of the anchor, so the anchor is announced once with both: “Inbox, button, 3 unread”. Each platform uses its own idiom: an `aria-description` on the anchor on the web, the accessibility value on Apple platforms, Flutter and React Native, and a state description on Compose.
- A count of 0 adds nothing: the anchor is announced as it would be without a badge.
- A badge with no anchor draws nothing, takes no space and says nothing, and a debug build reports it. Given an anchor later, it draws on it at once.
- A change of count or label updates what the anchor says, without an announcement that would interrupt the person.
- It takes no input and no space, so the anchor’s hit area stays whole.
- A dot without a label, a negative count and a `max` below 1 are reported in debug builds.

### What you need to do

- Write a label that completes the anchor’s name rather than repeating it: “3 unread”, not “Inbox has 3 unread messages”.
- Make the anchor one element with a name of its own, such as an icon button with a label. On Compose, a plain icon that is not inside an element that merges its descendants needs one around it for the name and the label to be read together.
- If a change must be announced as it happens, as in a live chat, announce it from your product with a live region. The badge does not announce.
