---
title: "徽标"
description: "在图标、导航项或头像上加一个圆点或一个小数字，表示有内容等待处理。"
url: "https://tanrei.chnotchy.com/zh-cn/components/badge/"
last_updated: 2026-10-26T22:31:46.000Z
---

# 徽标

> 在图标、导航项或头像上加一个圆点或一个小数字，表示有内容等待处理。

## 概览

### 用法

**Web**

```html
<!-- 没有 count 就是圆点。圆点必须有标签。 -->
<tanrei-badge label="有新消息">
  <tanrei-button variant="text" label="聊天"><span slot="icon">chat</span></tanrei-button>
</tanrei-badge>

<!-- 0 什么都不画；超过 max（999）的数字显示为 999+。 -->
<tanrei-badge count="3" label="3 封未读">
  <tanrei-button variant="text" label="收件箱"><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} 封未读`;
</script>
```

**React**

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

<Badge label="有新消息">
  <Button variant="text" label="聊天" onClick={openChat}><span slot="icon">chat</span></Button>
</Badge>
<Badge count={unread} label={`${unread} 封未读`}>
  <Button variant="text" label="收件箱" onClick={openInbox}><span slot="icon">inbox</span></Button>
</Badge>
```

**SwiftUI**

```swift
import Tanrei

// nil 就是圆点。
TanreiBadge(count: nil, label: "有新消息") {
  TanreiIconButton("聊天", action: openChat) { Image(systemName: "bubble.left") }
}
TanreiBadge(count: unread, label: "\(unread) 封未读") {
  TanreiIconButton("收件箱", action: openInbox) { Image(systemName: "tray") }
}

// 也可以作为修饰符加在任何视图上，效果相同。
Image(systemName: "bell").accessibilityLabel("通知").tanreiBadge(1200, label: "未读超过 999 条")
```

**Compose**

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

// null 就是圆点。
TanreiBadge(count = null, contentDescription = "有新消息") {
    TanreiIconButton(onClick = ::openChat, contentDescription = "聊天") { TanreiText("chat") }
}
TanreiBadge(count = unread, contentDescription = "$unread 封未读") {
    TanreiIconButton(onClick = ::openInbox, contentDescription = "收件箱") { TanreiText("inbox") }
}
```

**Flutter**

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

// 没有 count 就是圆点。
TanreiBadge(
  semanticLabel: '有新消息',
  child: TanreiIconButton(onPressed: openChat, semanticLabel: '聊天', icon: const Icon(AppSymbols.chat)),
);
TanreiBadge(
  count: unread,
  semanticLabel: '$unread 封未读',
  child: TanreiIconButton(onPressed: openInbox, semanticLabel: '收件箱', icon: const Icon(AppSymbols.inbox)),
);
```

**React Native**

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

<Badge label="有新消息">
  <IconButton accessibilityLabel="聊天" onPress={openChat}>chat</IconButton>
</Badge>
<Badge count={unread} label={`${unread} 封未读`}>
  <IconButton accessibilityLabel="收件箱" onPress={openInbox}>inbox</IconButton>
</Badge>
```

## 规格

- 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}` |

## 指南

### 何时使用

当图标、导航项或头像背后有内容等待处理时，用徽标来提示：未读消息、新动态、待安装的更新。它把视线引向用户可以处理这些内容的地方。徽标不带文字，在布局中也没有自己的位置，所以它不是状态标签。要用一个词说明状态或类别，例如“New”“Beta”或“Pro”，请使用[标签（Tag）](https://tanrei.chnotchy.com/zh-cn/components/tag/)。

### 徽标、纸片还是标签

三者按各自的作用来区分：

| 组件 | 它是什么 | 放在哪里 | 是否接受输入 |
| --- | --- | --- | --- |
| 徽标 | 附着在另一个元素上的圆点或数字。 | 在锚点的角上，布局之外。 | 否。输入会到达锚点。 |
| [纸片（Chip）](https://tanrei.chnotchy.com/zh-cn/components/chip/) | 一个控件。 | 在布局之内。 | 是：它可以被按下、切换或移除，并且有焦点和状态。 |
| [标签（Tag）](https://tanrei.chnotchy.com/zh-cn/components/tag/) | 一个静态的标签。 | 在布局之内，位于行内。 | 从不。 |

1. 如果它是叠在别的东西上面的数字或圆点，它就是徽标。
2. 如果它可以被按下、选中或移除，它就是纸片。如果它通向别处，它就是链接。
3. 如果它是一个词，只说明某个东西是什么或处于什么状态，它就是 Tag。

单独放在一行里的数字，例如标题旁边的结果数，同样不是徽标：徽标总有一个锚点，没有可依附之处的徽标什么也不画。这样的数字写成文本，与这一行的其他文字一起在原处朗读：在[导航抽屉](https://tanrei.chnotchy.com/zh-cn/components/navigation-drawer/)项的末尾，它就是该项的 `trailingText`。如果它说明的是一种状态，就写成 Tag。

### 如何选择形态

形态由 `count` 决定，所以没有变体可选。

| 形态 | `count` | 用途 |
| --- | --- | --- |
| 圆点 | 无（null） | 有新内容，而数量未知或无关紧要。 |
| 数字 | 大于 0 | 数量能帮助用户判断是否要现在查看：未读消息、待处理的请求。 |
| 不显示 | 0 | 没有内容在等待。把徽标直接绑定到数量上即可，不需要在外面加条件判断。 |

超过 `max` 的数字显示为 `max+`（默认为 999+），所以徽标永远不会超过 4 个字符。在锚点较小，或者精确数字更早失去意义的地方，可以把 `max` 调低（例如调到 99）。

### 推荐

- 给每个徽标一个接在锚点名称之后读起来通顺的标签：“收件箱”之后是“3 封未读”。圆点尤其需要标签，因为没有标签，圆点对屏幕阅读器来说什么也没说。
- 在锚点周围留出空间。徽标位于锚点顶部末尾侧的角上，可能超出锚点的边界；它从不移动锚点，也不改变锚点的大小。
- 用户看过徽标所指的内容之后，就清除徽标，这样它才始终有意义。

### 避免

- 不要给每一项都加徽标。全都做了标记，就没有哪一项显眼了。
- 不要把徽标放在按钮的标签上或连续文本之中。它属于图标、导航项或头像。标签旁边或一行文字里的词是 Tag。
- 不要用徽标的颜色来表示等待的是哪一类内容，或者有多紧急。这些信息写在标签里，或者写在它所指的内容里。
- 不要让徽标可以交互。在徽标上的轻点和点击会到达锚点，操作发生在锚点上。

## 无障碍

### 组件负责的部分

- 画出来的圆点和数字对辅助技术隐藏，所以数量不会被读两遍。
- 标签（没有标签时，则是显示出来的数字，例如“999+”）成为锚点的一部分，所以锚点连同两者只朗读一次：“收件箱，按钮，3 封未读”。每个平台使用自己的惯用方式：在 Web 上是锚点上的 `aria-description`，在 Apple 平台、Flutter 和 React Native 上是无障碍值，在 Compose 上是状态描述。
- 数量为 0 时不添加任何内容：锚点的朗读与没有徽标时相同。
- 没有锚点的徽标什么也不画、不占空间、也不表达任何内容，并在调试构建中报告。之后得到锚点时，会立即画在锚点上。
- 数量或标签变化时，锚点所表达的内容随之更新，但不会发出打断用户的朗读。
- 它不接受输入，也不占空间，所以锚点的点击区域保持完整。
- 没有标签的圆点、负的数量以及小于 1 的 `max` 会在调试构建中报告。

### 需要你做的事

- 写一个补全锚点名称而不是重复它的标签：写“3 封未读”，不写“收件箱有 3 封未读邮件”。
- 让锚点成为一个自带名称的单一元素，例如带标签的图标按钮。在 Compose 上，如果一个普通图标不在会合并其后代的元素之内，就需要在它外面包一个这样的元素，名称和标签才会被一起读出。
- 如果某个变化必须在发生时就朗读出来，例如在实时聊天中，请在你的产品里用实时区域（live region）来朗读。徽标本身不朗读。
