バッジ
アイコン、ナビゲーション項目、アバターにドットや小さな数を付けて、何かが待っていることを示します。
- カテゴリ
- コミュニケーション
- トークン
comp.badge- レベル
- 基礎
- Web
tanrei-badgeschedule - SwiftUI
TanreiBadgeschedule - Jetpack Compose
TanreiBadgeschedule - Flutter
TanreiBadgeschedule - React Native
Badgeschedule
生成元 docs/status/status.json, specs/badge.spec.json
ドットと数
<!-- 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>数と上限
構成要素
構成要素
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 |
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 | |
--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 | |
--tanrei-comp-badge-outline-color | sys.color.surface | |
--tanrei-comp-badge-outline-width | sys.border.thick | 2px |
--tanrei-comp-badge-label-typescale | sys.typescale.label-small | 500 12px/1.3333 |
フィクスチャ
基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。
dot
label: New messages count-single
count: 3 label: 3 unread count-double
count: 42 label: 42 unread count-overflow
count: 1200 label: More than 999 unread count-custom-max
count: 120 max: 99 label: More than 99 unread count-zero
count: 0 label: No unread messages 生成元 specs/badge.spec.json, tokens/comp/badge.tokens.json, design/tokens/
アイコン、ナビゲーション項目、アバターの先に何かが待っていることを示すときに使います。未読のメッセージ、新しいアクティビティ、インストールできる更新などです。バッジは、操作できる場所へ視線を導きます。バッジには言葉がなく、レイアウトの中に自分の場所も持たないので、状態を表すラベルではありません。「新着」「ベータ」「Pro」のように、状態や分類を言葉で示すものには、タグを使います。
この 3 つは、何をするものかで区別します。
| 種類 | 何であるか | どこに置かれるか | 入力を受けるか |
|---|---|---|---|
| バッジ | ほかの要素に付くドットや数。 | アンカーの角の上。レイアウトの外です。 | 受けません。入力はアンカーに届きます。 |
| チップ | コントロール。 | レイアウトの中。 | 受けます。押す、切り替える、取り除くことができ、フォーカスと状態を持ちます。 |
| タグ | 静的なラベル。 | レイアウトの中、行内。 | 受けません。 |
- 何かの上に載る数やドットは、バッジです。
- 押す、選ぶ、取り除くものは、チップです。どこかへ移動するものは、リンクです。
- それが何であるか、どんな状態かを言葉で示すだけのものは、タグです。
見出しの横の検索結果の件数のように、行の中に単独で置く数も、バッジではありません。バッジには必ずアンカーがあり、載せる先のないバッジは何も描きません。そのような数はテキストとして書き、行のほかの言葉と一緒にその場所で読まれるようにします。ナビゲーションドロワーの項目の末尾なら、その項目の trailingText です。状態を示すものならタグにします。
形は count で決まるので、選ぶバリアントはありません。
| 形 | count |
用途 |
|---|---|---|
| ドット | なし(null) | 何か新しいものがあり、その数は分からないか重要でないとき。 |
| 数 | 1 以上 | 今見るべきかどうかを判断するのに数が役立つとき。未読メッセージ、保留中のリクエストなど。 |
| なし | 0 | 待っているものがないとき。バッジは数にそのまま結び付けます。条件分岐で囲む必要はありません。 |
max を超える数は max+(既定値では 999+)と表示するので、バッジが 4 文字より長くなることはありません。アンカーが小さい場所や、正確な数がそれほど意味を持たない場所では、max を小さくします(たとえば 99)。
- アンカーの名前に続けて自然に読めるラベルを、どのバッジにも付けます。「受信トレイ」に続く「未読 3 件」のようにです。とくにドットには必須です。ラベルがないと、スクリーンリーダーには何も伝わりません。
- アンカーの周りに余白を取ります。バッジはアンカーの上の末尾側の角に載り、アンカーの範囲からはみ出すことがあります。アンカーの位置や大きさは変えません。
- バッジが指す内容をユーザーが見たら、バッジを消します。そうすることで、バッジが意味を持ち続けます。
- すべての項目にバッジを付けない。すべてに印があると、どれも目立たなくなります。
- ボタンのラベルや本文の中にバッジを置かない。バッジを置くのは、アイコン、ナビゲーション項目、アバターです。ラベルの横や文章の中に置く言葉は、タグです。
- 待っているものの種類や緊急度を、バッジの色で伝えない。ラベルか、バッジが指す先の内容で伝えます。
- バッジを操作できるようにしない。バッジへのタップやクリックはアンカーに届きます。操作する場所はアンカーです。
- ロール
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+」のように表示している数)がアンカーの一部になり、アンカーは両方を合わせて 1 度だけ読み上げられます。たとえば「受信トレイ、ボタン、未読 3 件」です。方法は各プラットフォームの流儀に従います。Web ではアンカーの
aria-description、Apple のプラットフォーム、Flutter、React Native ではアクセシビリティの値、Compose では状態の説明(state description)です。 - 数が 0 のときは何も加えません。アンカーはバッジがないときと同じように読み上げられます。
- アンカーのないバッジは何も描かず、場所も取らず、何も伝えません。デバッグビルドではそれを報告します。あとからアンカーが与えられると、すぐにその上に描きます。
- 数やラベルが変わると、アンカーが伝える内容を更新します。ユーザーの作業を遮る読み上げはしません。
- 入力も場所も取らないので、アンカーのヒット領域はそのまま保たれます。
- ラベルのないドット、負の数、1 未満の
maxは、デバッグビルドで報告します。
- アンカーの名前を繰り返さず、続きとして読めるラベルを書きます。「受信トレイに未読メッセージが 3 件あります」ではなく「未読 3 件」です。
- アンカーは、ラベル付きのアイコンボタンのように、自分の名前を持つ 1 つの要素にします。Compose では、子孫をまとめる要素の中にない単なるアイコンに付ける場合、名前とラベルが一緒に読まれるように、それをまとめる要素で囲みます。
- ライブチャットのように、変化をその場で読み上げる必要があるときは、プロダクト側でライブリージョンを使って知らせます。バッジ自身は読み上げません。