跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件徽标

徽标

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

content_copy以 Markdown 格式查看
类别
信息传达
令牌
comp.badge
层级
基础

概览

Web
tanrei-badge
schedule
SwiftUI
TanreiBadge
schedule
Jetpack Compose
TanreiBadge
schedule
Flutter
TanreiBadge
schedule
React Native
Badge
schedule

生成自 docs/status/status.json, specs/badge.spec.json

圆点和数字

chatinboxnotifications

用法

<!-- 没有 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>
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>
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 条")
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") }
}
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)),
);
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>

规格

数字和上限

notificationsnotificationsnotificationsnotificationsnotificationsnotifications

构成

构成

绘制在 Web 包所绘的测试样例 count-single 之上。
部件 说明
1 container The pill that holds the count: at least as wide as it is high, so a single digit sits in a circle.
2 dot · 可选 The container's form when there is no count: a small circle with no label.
3 label · 可选 The count, or the capped count followed by a plus sign.
4 outline A ring outside the dot or the pill that separates the badge from what it overlaps.

尺寸

尺寸

长度 令牌 值(px)
container padding-inline tanrei.comp.badge.container.padding-inline 4
container height tanrei.comp.badge.container.height 16
绘制在 Web 包所绘的测试样例 count-single 之上。

属性

属性 类型 默认值 说明
count number null 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 The largest count shown as it is; a larger one shows as max followed by a plus sign (999+).
label string "" What assistive technology reads after the anchor's own name ("3 unread"). With no label, the displayed count is read; a dot needs one.

插槽

插槽 说明
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").

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

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.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-badge TanreiBadge TanreiBadge TanreiBadge Badge
count · 属性 count count count count count
max · 属性 max max max max max
label · 属性 label label contentDescription semanticLabel label
备注 Wraps its anchor in the default slot. No count attribute is the dot. The badge sets aria-description on the slotted anchor and keeps its own shadow content aria-hidden. TanreiBadge(count: Int?, max:, label:) { anchor }, with a .tanreiBadge(_:max:label:) modifier for the same view (SwiftUI's own .badge works only in lists and tab bars). count: nil is the dot. One composable plays Material's BadgedBox and Badge together: TanreiBadge(count: Int?, max, contentDescription) { anchor }. count = null is the dot. As Flutter's Badge.count, with the anchor as child; count: null is the dot (Flutter's plain Badge). The anchor is children. label is appended to the anchor's accessibilityValue.text, not set as the badge's own accessibilityLabel: the badge itself is not an accessibility element.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-badge-container-color sys.color.primary #1b1b1b
--tanrei-comp-badge-container-height — 16px
--tanrei-comp-badge-container-offset sys.space.space0 0px
--tanrei-comp-badge-container-padding-inline sys.space.space50 4px
--tanrei-comp-badge-container-shape sys.shape.corner.full 9999px
--tanrei-comp-badge-dot-offset sys.space.space25 2px
--tanrei-comp-badge-dot-size — 6px
--tanrei-comp-badge-label-color sys.color.on-primary #ffffff
--tanrei-comp-badge-outline-color sys.color.surface #ffffff
--tanrei-comp-badge-outline-width sys.border.thick 2px
--tanrei-comp-badge-label-typescale sys.typescale.label-small 500 12px/1.3333

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/badge.spec.json, tokens/comp/badge.tokens.json, design/tokens/

指南

何时使用

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

徽标、纸片还是标签

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

组件 它是什么 放在哪里 是否接受输入
徽标 附着在另一个元素上的圆点或数字。 在锚点的角上,布局之外。 否。输入会到达锚点。
纸片(Chip) 一个控件。 在布局之内。 是:它可以被按下、切换或移除,并且有焦点和状态。
标签(Tag) 一个静态的标签。 在布局之内,位于行内。 从不。
  1. 如果它是叠在别的东西上面的数字或圆点,它就是徽标。
  2. 如果它可以被按下、选中或移除,它就是纸片。如果它通向别处,它就是链接。
  3. 如果它是一个词,只说明某个东西是什么或处于什么状态,它就是 Tag。

单独放在一行里的数字,例如标题旁边的结果数,同样不是徽标:徽标总有一个锚点,没有可依附之处的徽标什么也不画。这样的数字写成文本,与这一行的其他文字一起在原处朗读:在导航抽屉项的末尾,它就是该项的 trailingText。如果它说明的是一种状态,就写成 Tag。

如何选择形态

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

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

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

推荐

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

避免

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

无障碍

角色
none
标注
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 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作

备注

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

生成自 specs/badge.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

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

需要你做的事

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