ツールバー
関連するコントロールをひとまとめにし、Tab で止まる位置を 1 か所にします。コンテンツが必要とする場所にインラインで置くか、画面の下端に固定するか、コンテンツの上に浮かせて置きます。コントロールの間は矢印キーで移動し、収まらないものはオーバーフローメニューに入ります。
- カテゴリ
- アクション
- トークン
comp.toolbar- レベル
- 基礎
- Web
tanrei-toolbarschedule - SwiftUI
TanreiToolbarschedule - Jetpack Compose
TanreiToolbarschedule - Flutter
TanreiToolbarschedule - React Native
Toolbarschedule
生成元 docs/status/status.json, specs/toolbar.spec.json
インライン、ドック、フローティング
ツールバーは、関連するコントロールをひとまとめにします。アクション、トグル、メニューを開くボタン、メニュー付きボタンなどです。項目は利用者側で用意する Tanrei のボタンとメニュー付きボタンそのもので、それぞれが自分のラベル、クリック、状態を持ちます。ツールバーはそれらを読み順に並べ、グループの間を区切り、キーボードフォーカスをその間で動かし、収まらないものをオーバーフローメニューに隠します。見た目を変えることはないので、ツールバーの中のアイコンだけのボタンも、ほかの場所と同じ見た目で同じように動きます。
項目は 6 種類です。
action:アイコンだけのボタン。太字のようなトグルは、toggleを付けたものです。button:ラベルのあるボタン。オンとオフを切り替える絞り込みのように、これもトグルにできます。menu-button:Tanrei のメニューをアンカーしたボタン。並べ替えや挿入などです。メニューは利用者側のもので、そのボタンの隣に置きます。ツールバーはメニューを並べません。button-with-menu:メインのアクションと、関連するアクションのメニュー。左右それぞれが、矢印キーで止まる位置になります。group:太字、斜体、下線のような、関連する項目のひと続き。自分では何も描きません。まとまりがその人にとって意味を持つときは、「文字のスタイル」のようにlabelを付けます。separator:グループに入っていない 2 つの項目の間の線。Web では区切り線で、ツールバーは 2 つのグループの間の線と同じように描きます。
インライン(既定)。 インラインのツールバーは、利用者側のレイアウトが置いた場所、つまり働きかける対象のコンテンツの中に置かれます。表の上、エディターの上端、絞り込みの並びとしてリストの横などです。長さは項目が必要とする分だけで、レイアウトが許す長さを超えることはありません。既定のシステムでは、自分のサーフェスも影も縁も持たず、両端は周りのコンテンツとそろいます。
ドック。 ドックのツールバーは画面の下端いっぱいに広がります。色はホームインジケーターやジェスチャーバーの下まで届き、項目はその上、左右のインセットの内側に収まります。Web は env(safe-area-inset-bottom) を読み、SwiftUI では .safeAreaInset(edge: .bottom) でバーを置き、Compose は windowInsets(既定はナビゲーションバー)を受け取り、Flutter は下側の MediaQuery のパディングを読みます。React Native では、Tanrei がセーフエリアのライブラリに依存しないため、インセットはツールバーの insets プロパティか TanreiProvider から受け取ります。
フローティング。 フローティングのツールバーは、メインアクションボタンと同じく自分では位置を決めません。置き場所は利用者側のレイアウトで決めます。ふつうはコンテンツの下端の少し上に中央揃えで、縦向きならコンテンツの末尾側の端の横に置きます。
Web と React の例は現在の仕様に沿っています。SwiftUI、Compose、Flutter、React Native は追従待ちで、ツールバーとボタンの以前の版を提供しています。そこでは、ツールバーは指定しない限りドックで、入れられるのはボタンだけ、収まらないものはスクロールし、アイコンだけのアクションは別のアイコンボタンです。追いつくまで、これらの例はその版のものです。
<tanrei-toolbar label="書式" overflow-label="その他の書式">
<tanrei-toolbar-group label="文字のスタイル">
<tanrei-button variant="text" toggle selected label="太字"><span slot="icon">format_bold</span></tanrei-button>
<tanrei-button variant="text" toggle label="斜体"><span slot="icon">format_italic</span></tanrei-button>
</tanrei-toolbar-group>
<tanrei-toolbar-group>
<tanrei-button variant="text" label="リンクを挿入"><span slot="icon">link</span></tanrei-button>
<tanrei-button variant="text" id="insert" label="挿入"><span slot="icon">add</span></tanrei-button>
<tanrei-menu anchor="insert">
<tanrei-menu-item label="表"></tanrei-menu-item>
<tanrei-menu-item label="水平線"></tanrei-menu-item>
</tanrei-menu>
</tanrei-toolbar-group>
<tanrei-divider></tanrei-divider>
<tanrei-button variant="filled" toolbar-overflow="never">公開</tanrei-button>
</tanrei-toolbar>
<!-- 画面の下端いっぱいにドックします。 -->
<tanrei-toolbar variant="docked" label="メッセージのアクション">
<tanrei-button variant="text" label="アーカイブ"><span slot="icon">archive</span></tanrei-button>
<tanrei-button variant="text" label="削除"><span slot="icon">delete</span></tanrei-button>
<tanrei-button variant="filled">返信</tanrei-button>
</tanrei-toolbar>
<script type="module">
import '@tanrei/web/components/toolbar';
import '@tanrei/web/components/button';
import '@tanrei/web/components/divider';
import '@tanrei/web/components/menu';
// 項目は利用者側のものです。ツールバー自身はイベントを出しません。
document.querySelector('[label="リンクを挿入"]').addEventListener('click', insertLink);
const insert = document.querySelector('tanrei-menu[anchor="insert"]');
document.querySelector('#insert').addEventListener('click', () => (insert.open = !insert.open));
insert.addEventListener('close', () => (insert.open = false));
</script>import { Button, Divider, Toolbar, ToolbarGroup } from '@tanrei/react';
<Toolbar label="書式" overflowLabel="その他の書式">
<ToolbarGroup label="文字のスタイル">
<Button variant="text" toggle selected={bold} label="太字" onChange={(e) => setBold(e.detail.selected)}><span slot="icon">format_bold</span></Button>
<Button variant="text" toggle selected={italic} label="斜体" onChange={(e) => setItalic(e.detail.selected)}><span slot="icon">format_italic</span></Button>
</ToolbarGroup>
<ToolbarGroup>
<Button variant="text" label="リンクを挿入" onClick={insertLink}><span slot="icon">link</span></Button>
</ToolbarGroup>
<Divider />
<Button variant="filled" toolbar-overflow="never" onClick={publish}>公開</Button>
</Toolbar>
<Toolbar variant="docked" label="メッセージのアクション">
<Button variant="text" label="アーカイブ" onClick={archive}><span slot="icon">archive</span></Button>
<Button variant="filled" onClick={reply}>返信</Button>
</Toolbar>import Tanrei
// ドックのツールバーは下側のセーフエリアのインセットに置きます。背景はその下まで届きます。
MessageView()
.safeAreaInset(edge: .bottom, spacing: 0) {
TanreiToolbar("メッセージのアクション") {
TanreiIconButton("アーカイブ", action: archive) { Image(systemName: "archivebox") }
TanreiIconButton("削除", action: delete) { Image(systemName: "trash") }
TanreiButton("返信", action: reply)
}
}
TanreiToolbar("書式", variant: .floating) {
TanreiIconButton("太字", isSelected: $bold) { Image(systemName: "bold") }
TanreiIconButton("リンクを挿入", action: insertLink) { Image(systemName: "link") }
}import androidx.compose.foundation.gestures.Orientation
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.components.TanreiToolbar
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiToolbarVariant
// ドックのツールバーは、windowInsets を渡さなければ WindowInsets.navigationBars の分だけ項目を内側に寄せます。
TanreiToolbar(contentDescription = "メッセージのアクション") {
TanreiIconButton(onClick = ::archive, contentDescription = "アーカイブ") { TanreiText("archive") }
TanreiIconButton(onClick = ::delete, contentDescription = "削除") { TanreiText("delete") }
TanreiButton(text = "返信", onClick = ::reply)
}
TanreiToolbar(contentDescription = "書式", variant = TanreiToolbarVariant.Floating) {
TanreiIconButton(onClick = {}, contentDescription = "太字", checked = bold, onCheckedChange = { bold = it }) {
TanreiText("format_bold")
}
TanreiIconButton(onClick = ::insertLink, contentDescription = "リンクを挿入") { TanreiText("link") }
}
// 縦向きはフローティングのツールバー用です。置き場所はレイアウトで決めます。
TanreiToolbar(
contentDescription = "地図の操作",
variant = TanreiToolbarVariant.Floating,
orientation = Orientation.Vertical,
) {
TanreiIconButton(onClick = ::zoomIn, contentDescription = "拡大") { TanreiText("add") }
TanreiIconButton(onClick = ::zoomOut, contentDescription = "縮小") { TanreiText("remove") }
}import 'package:tanrei/tanrei.dart';
TanreiToolbar(
semanticLabel: 'メッセージのアクション',
children: <Widget>[
TanreiIconButton(onPressed: archive, semanticLabel: 'アーカイブ', icon: const Icon(AppSymbols.archive)),
TanreiIconButton(onPressed: delete, semanticLabel: '削除', icon: const Icon(AppSymbols.delete)),
TanreiButton(onPressed: reply, child: const Text('返信')),
],
);
TanreiToolbar(
semanticLabel: '地図の操作',
variant: TanreiToolbarVariant.floating,
orientation: Axis.vertical,
children: <Widget>[
TanreiIconButton(onPressed: zoomIn, semanticLabel: '拡大', icon: const Icon(AppSymbols.add)),
TanreiIconButton(onPressed: zoomOut, semanticLabel: '縮小', icon: const Icon(AppSymbols.remove)),
],
);import { Button, IconButton, Toolbar } from '@tanrei/react-native';
// 下側と左右のインセットは TanreiProvider の insets から受け取ります。ツールバー自身の insets プロパティは、指定した辺だけそれを置き換えます。
<Toolbar accessibilityLabel="メッセージのアクション">
<IconButton accessibilityLabel="アーカイブ" onPress={archive}>archive</IconButton>
<IconButton accessibilityLabel="削除" onPress={remove}>delete</IconButton>
<Button variant="filled" onPress={reply}>返信</Button>
</Toolbar>
<Toolbar variant="floating" accessibilityLabel="書式">
<IconButton accessibilityLabel="太字" toggle selected={bold} onSelectedChange={setBold}>format_bold</IconButton>
<IconButton accessibilityLabel="リンクを挿入" onPress={insertLink}>link</IconButton>
</Toolbar>グループと区切り、メニューボタン、メニュー付きボタン、ラベル付きのトグル、オーバーフローメニュー
構成要素
| パーツ | 説明 |
|---|---|
1 container | The surface that holds the items: none of its own in the default system where the toolbar stands inline in the content; a bar across the bottom edge of the screen whose color reaches under the bottom system inset (docked); or a rounded, raised surface over the content (floating). |
2 items | The controls, in reading order: the product's own Tanrei Buttons, icon-only and labelled, toggles among them, Buttons that open a Menu, and Buttons with menu, alone or in groups. The toolbar lays them out and moves keyboard focus among them; it does not restyle them. |
3 separator · 任意 | A line across the toolbar between two groups, or where a separator item stands, or the empty space that takes its place, as the design system's separation says. |
4 overflow-button · 任意 | The icon-only Button at the end that opens a Menu of the items that do not fit, drawn only while some do not. |
5 outline | The docked toolbar's top edge, or the edge around a floating or an inline toolbar's whole shape (none in the default system for the last two). |
プロパティ
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
variant | inline | docked | floating | "inline" | Inline stands in place where the layout puts it, as long as its items need and no longer than the layout allows, with no surface, shadow or edge of its own in the default system (an editor's formatting bar, a table's actions, a row of filters); docked is a full-width bar along the bottom edge of the screen, flush with it; floating is a rounded, raised surface over the content, as long as its items. One product places a toolbar inside its content on one screen, docks it on another and floats it on a third. A design system may add values. |
orientation | horizontal | vertical | "horizontal" | The direction the items run. Vertical is for an inline or a floating toolbar beside the content (a floating one on a large screen); a docked toolbar is always horizontal. |
label | string | "" | Names the toolbar for assistive technology ("Formatting", "Message actions"). Never drawn. Required. |
overflow | menu | scroll | "menu" | What happens when the items do not fit along the toolbar's axis: menu hides items from the end into an overflow menu at the end of the toolbar (TBR-18); scroll keeps every item and scrolls them (TBR-8). |
overflowLabel | string | "" | The overflow button's accessible name ("More actions"), in the product's language. Never drawn. Empty takes the product's words. Needed whenever the toolbar may be narrower than its items. |
separation | auto | divider | gap | "auto" | How two groups, or a separator item, set items apart: divider draws a line across the toolbar with space on each side, gap leaves empty space and no line. Auto takes comp.toolbar.separation, the design system's answer (divider in the default system). |
スロット
| スロット | 説明 |
|---|---|
default | Holds the items, in reading order: Tanrei Buttons, icon-only and labelled, Buttons with a Tanrei Menu anchored to them (the Menu beside its Button), Buttons with menu, groups of them and separators. |
アイテム
このコンポーネントが繰り返すアイテムの種類です。フィクスチャはアイテムを順に並べ、各プラットフォームはそれぞれの流儀で種類ごとに名前を付けます。
action
An icon action: a Button of the text variant and the medium size with an icon and no label, unless the product sets otherwise, laid out by the toolbar and not restyled; a toggle (bold, mute) is one with toggle set.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
label | string | "" | Its accessible name ("Bold"), as the Button's label. Required. |
variant | filled | tonal | elevated | outlined | text | "text" | As the Button's variant: text for most actions, filled for the one the toolbar leads to (Done, Send). |
toggle | boolean | false | As the Button's toggle: the action switches something on and off. |
selected | boolean | false | As the Button's selected: whether a toggle is on. |
disabled | boolean | false | As the Button's disabled. |
overflow | auto | never | "auto" | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |
| スロット | 説明 |
|---|---|
icon | Its glyph, as the Button's icon slot. A fixture item's content is this glyph. |
| イベント | 説明 |
|---|---|
click | The action was activated, as the Button's click. |
change | A toggle was switched, as the Button's change. |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-button | TanreiButton | TanreiButton | TanreiButton | Button |
label · プロパティ | label | label | contentDescription | semanticLabel | accessibilityLabel |
variant · プロパティ | variant | variant | variant | variant | variant |
toggle · プロパティ | toggle | isSelected | onCheckedChange | isSelected | toggle |
selected · プロパティ | selected | isSelected | checked | isSelected | selected |
disabled · プロパティ | disabled | disabled | enabled | onPressed | disabled |
overflow · プロパティ | toolbar-overflow | toolbarOverflow | Modifier.toolbarOverflow | TanreiToolbarItem.overflow | toolbarOverflow |
click · イベント | click | action | onClick | onPressed | onPress |
change · イベント | change | isSelected | onCheckedChange | onPressed | onSelectedChange |
| 備考 | An icon-only <tanrei-button> child; the glyph is its content. | An icon-only TanreiButton in the toolbar's view builder; a toggle is its isSelected: initializer. | An icon-only TanreiButton called in the toolbar's content lambda. | An icon-only TanreiButton in the toolbar's children. | An icon-only Button element among the toolbar's children. |
button
A labelled action: the button component, medium unless the product sets otherwise, laid out by the toolbar and not restyled.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
variant | filled | tonal | elevated | outlined | text | "filled" | As the button's variant. |
toggle | boolean | false | As the button's toggle: a labelled toggle (a filter that is on or off). |
selected | boolean | false | As the button's selected: whether a toggle is on. |
disabled | boolean | false | As the button's disabled. |
overflow | auto | never | "auto" | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |
| スロット | 説明 |
|---|---|
default | Its label, as the button's default slot. A fixture item's content is this label. |
| イベント | 説明 |
|---|---|
click | The button was activated, as the button's click. |
change | A toggle was switched, as the button's change. |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-button | TanreiButton | TanreiButton | TanreiButton | Button |
variant · プロパティ | variant | variant | variant | variant | variant |
toggle · プロパティ | toggle | isSelected | onCheckedChange | isSelected | toggle |
selected · プロパティ | selected | isSelected | checked | isSelected | selected |
disabled · プロパティ | disabled | disabled | enabled | onPressed | disabled |
overflow · プロパティ | toolbar-overflow | toolbarOverflow | Modifier.toolbarOverflow | TanreiToolbarItem.overflow | toolbarOverflow |
click · イベント | click | action | onClick | onPressed | onPress |
change · イベント | change | isSelected | onCheckedChange | onPressed | onSelectedChange |
| 備考 | A <tanrei-button> child; the label is its content. | A TanreiButton in the toolbar's view builder. | A TanreiButton called in the toolbar's content lambda. | A TanreiButton in the toolbar's children. | A Button element among the toolbar's children. |
menu-button
A Button that opens a Tanrei Menu anchored to it (Sort, Insert): icon-only unless the product sets otherwise, laid out by the toolbar as any other item and not restyled. The Menu is the product's, beside it in the toolbar; it is neither laid out nor an item.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
label | string | "" | Its accessible name ("Sort"), as the Button's label. Required. |
variant | filled | tonal | elevated | outlined | text | "text" | As the Button's variant. |
disabled | boolean | false | As the Button's disabled. |
overflow | auto | never | "auto" | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |
| スロット | 説明 |
|---|---|
icon | Its glyph, as the Button's icon slot. A fixture item's content is this glyph. |
| イベント | 説明 |
|---|---|
click | The button was activated, as the Button's click: the product opens its Menu. |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-button | TanreiButton | TanreiButton | TanreiButton | Button |
label · プロパティ | label | label | contentDescription | semanticLabel | accessibilityLabel |
variant · プロパティ | variant | variant | variant | variant | variant |
disabled · プロパティ | disabled | disabled | enabled | onPressed | disabled |
overflow · プロパティ | toolbar-overflow | toolbarOverflow | Modifier.toolbarOverflow | TanreiToolbarItem.overflow | toolbarOverflow |
click · イベント | click | action | onClick | onPressed | onPress |
| 備考 | A <tanrei-button id> child with a <tanrei-menu anchor="<its id>"> beside it, in the toolbar or in the same group. | A TanreiButton with .tanreiMenu { … } attached, in the toolbar's view builder. | A TanreiButton with a TanreiMenu anchored to it, called in the toolbar's content lambda. | A TanreiButton that opens a TanreiMenu anchored to it, in the toolbar's children. | A Button with a Menu anchored to it, among the toolbar's children. |
button-with-menu
A Button with menu: a main action and a menu of related ones, each half a stop of the toolbar's arrow keys (TBR-25), laid out by the toolbar and not restyled.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
label | string | "" | The leading half's label ("Save"), as the Button with menu's label. |
menuLabel | string | "" | The trailing half's accessible name ("More save options"), as the Button with menu's menuLabel. |
variant | filled | tonal | elevated | outlined | "filled" | As the Button with menu's variant. |
disabled | boolean | false | As the Button with menu's disabled. |
overflow | auto | never | "auto" | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |
| イベント | 説明 |
|---|---|
click | The main action was activated, as the Button with menu's click. |
toggle | The trailing half asks to open or close the menu, as the Button with menu's toggle. |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-button-with-menu | TanreiButtonWithMenu | TanreiButtonWithMenu | TanreiButtonWithMenu | ButtonWithMenu |
label · プロパティ | label | title | text | label | label |
menuLabel · プロパティ | menuLabel | menuLabel | menuContentDescription | menuSemanticLabel | menuAccessibilityLabel |
variant · プロパティ | variant | variant | variant | variant | variant |
disabled · プロパティ | disabled | disabled | enabled | onPressed | disabled |
overflow · プロパティ | toolbar-overflow | toolbarOverflow | Modifier.toolbarOverflow | TanreiToolbarItem.overflow | toolbarOverflow |
click · イベント | click | action | onClick | onPressed | onPress |
toggle · イベント | toggle | isMenuPresented | onMenuExpandedChange | menuController | onToggle |
| 備考 | A <tanrei-button-with-menu> child, its menu items in slot="menu" as anywhere. | A TanreiButtonWithMenu in the toolbar's view builder. | A TanreiButtonWithMenu called in the toolbar's content lambda. | A TanreiButtonWithMenu in the toolbar's children. | A ButtonWithMenu among the toolbar's children. |
group · グループ — アイテムを含む
A run of related items (Bold, Italic, Underline), optionally named; the toolbar sets it apart from what is next to it as separation says. It draws nothing of its own.
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
label | string | "" | Names the group for assistive technology ("Text style"); never drawn. Empty leaves the group unexposed: its items are the toolbar's. |
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-toolbar-group | TanreiToolbarGroup | TanreiToolbarGroup | TanreiToolbarGroup | ToolbarGroup |
label · プロパティ | label | label | contentDescription | semanticLabel | accessibilityLabel |
| 備考 | A <tanrei-toolbar-group> child holding items: display: contents, with role="group" and aria-label while its label is not empty. | TanreiToolbarGroup(_ label:) { items } in the toolbar's view builder: an accessibility container named by its label, or none without one. | TanreiToolbarGroup(contentDescription) { … } in the toolbar's content lambda: a traversal group named by its description, or none without one. | TanreiToolbarGroup(semanticLabel:, children:) in the toolbar's children: a semantics container named by its label, or none without one. | <ToolbarGroup accessibilityLabel>{items}</ToolbarGroup> among the toolbar's children: a view with role group while it has a label. |
separator
A line across the toolbar between two items outside the groups: the toolbar draws it as it draws the line between two groups, and it is never a stop of the arrow keys.
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| アイテム | tanrei-divider | TanreiDivider | TanreiDivider | TanreiDivider | Divider |
| 備考 | A <tanrei-divider> child: the toolbar draws the line itself, as between two groups (TBR-23), as an element of its own with role="separator", and leaves the divider unassigned. | A TanreiDivider() in the toolbar's view builder; the toolbar draws the line from its own tokens. | A TanreiDivider() called in the toolbar's content lambda; the toolbar draws the line from its own tokens. | A TanreiDivider() in the toolbar's children; the toolbar draws the line from its own tokens. | A Divider among the toolbar's children; the toolbar draws the line from its own tokens. |
状態
振る舞い
EARS 記法による要件です。各プラットフォームの実装は、この ID に対応するテストで検証されます。要件の原文は仕様ファイルのものをそのまま掲載しています。
| ID | 要件 |
|---|---|
TBR-1 | WHILE variant is docked THE toolbar SHALL span the full width of its parent, be at least comp.toolbar.container.size.docked high above the bottom system inset, fill its container with comp.toolbar.container.color.docked, raise it by comp.toolbar.container.elevation.docked, round it by comp.toolbar.container.shape.docked, and draw its top edge comp.toolbar.outline.width thick in comp.toolbar.outline.color.docked. |
TBR-2 | WHILE variant is docked THE toolbar SHALL lay its items out in one row in reading order, the first comp.toolbar.container.padding-inline.docked from the start edge and the last as far from the end edge, with equal space between neighbours and never less than comp.toolbar.container.gap.docked, each item vertically centered in the row; a toolbar with one item SHALL center it. |
TBR-3 | WHILE variant is docked THE toolbar SHALL extend its container under the bottom system inset (home indicator, gesture bar, taskbar) and SHALL place its items above that inset and inside the left and right insets, so that its color reaches the screen's bottom edge while no item sits under a system bar. |
TBR-4 | WHILE variant is floating THE toolbar SHALL fill its container with comp.toolbar.container.color.floating, raise it by comp.toolbar.container.elevation.floating, round it by comp.toolbar.container.shape.floating eased by sys.shape.corner-smoothing, draw an edge comp.toolbar.outline.width thick in comp.toolbar.outline.color.floating around its whole shape, be as long along its axis as its items need — comp.toolbar.container.padding-inline.floating at each end and comp.toolbar.container.gap.floating between neighbours, or as much more as TBR-16 keeps for their targets — and at least comp.toolbar.container.size.floating across it, with every item centered across the axis. |
TBR-5 | WHILE variant is floating THE toolbar SHALL NOT position itself: it SHALL lay out at its own size where the layout that holds it places it (normally centered above the bottom edge of the content, clear of a navigation bar and of the system inset, or beside the content's end edge for a vertical toolbar), and SHALL NOT draw a scrim, move content or reach under a system inset. |
TBR-6 | WHILE orientation is vertical and variant is floating or inline THE toolbar SHALL stack its items from top to bottom in the order given, with its variant's paddings and gap along the vertical axis and at least its variant's container size as its width; WHILE orientation is vertical and variant is docked THE toolbar SHALL lay out as a horizontal docked toolbar and in debug builds report that a docked toolbar is horizontal. |
TBR-7 | WHEN the layout direction is right-to-left THE toolbar SHALL lay out a horizontal toolbar's items from the right edge, in reading order, mirrored; a vertical toolbar SHALL keep its top-to-bottom order. |
TBR-8 | WHILE overflow is scroll and the items need more length than the toolbar has along its axis THE toolbar SHALL keep every item at its own size and the gap at its minimum, SHALL scroll its items along its axis so that every item stays reachable, and SHALL bring an item that receives keyboard focus fully into view; it SHALL NOT wrap, shrink or hide an item. |
TBR-9 | THE toolbar SHALL NOT restyle its items: each SHALL render as the Button it is, from its own tokens, variant and size, and SHALL keep its own press, focus, target and disabled behaviour and its own semantics, a toggle Button staying a toggle. Between two items the hit areas SHALL never overlap (as BGR-4): in a docked toolbar they SHALL meet at the middle of the gap, and in a floating or an inline one where the lengths TBR-16 keeps for the two items meet; each item SHALL keep its full target on every side that has no neighbour. In a docked toolbar the gap (at least comp.toolbar.container.gap.docked) is what keeps an item's target whole between its neighbours: at sys.target.touch minus the item's length or more, every item keeps its full target there, as the default system's gap does for every Button size; a layer that narrows that gap narrows the targets between items. In a floating or an inline toolbar TBR-16 keeps it, whatever comp.toolbar.container.gap.floating or comp.toolbar.container.gap.inline is. |
TBR-10 | THE toolbar SHALL be one Tab stop: the item that had keyboard focus last, or else the first item that is not disabled. Its items are its own and its groups' items, in reading order; a separator and a Menu anchored to an item are none. WHILE an item has keyboard focus, Right Arrow and Left Arrow in a horizontal toolbar (Left Arrow moving to the next item in a right-to-left layout) and Down Arrow and Up Arrow in a vertical one SHALL move focus to the next and previous item that is not disabled, wrapping at the ends, and Home and End to the first and last such item; moving focus SHALL NOT activate an item, and Enter and Space SHALL act on the focused item as that item does. A disabled item SHALL NOT take focus, as its own spec says (BTN-2). |
TBR-11 | THE toolbar SHALL expose itself as a toolbar named by label, with its orientation, on a platform that has a toolbar role, and on any other platform as a group named by label that contains its items in order; each item SHALL keep its own name, role and state, and the toolbar SHALL add nothing to them. |
TBR-12 | WHILE label is empty THE toolbar SHALL still render, and in debug builds report that it has no accessible name. IF the toolbar holds no item, or an element that is neither one of its item kinds nor a Menu anchored to one of its items, THE toolbar SHALL still lay it out, and in debug builds report it, because a field or a slider would take the arrow keys the toolbar moves focus with (the WAI-ARIA toolbar pattern). |
TBR-13 | WHEN variant names a value that the active design system does not have, or orientation a value the component does not define, THE toolbar SHALL render with the default value (inline, horizontal), and in debug builds report the unknown value. A variant value that a layer adds SHALL take the inline structure with its own values, because the structure is not a value a layer can set. WHEN overflow or separation names a value the component does not define THE toolbar SHALL render with menu or auto, and in debug builds report it. |
TBR-14 | WHILE the text size is enlarged THE toolbar SHALL grow across its axis to hold its items (its size is a minimum) and SHALL NOT scale its items itself; each item grows or keeps its size as its own spec says. |
TBR-15 | THE toolbar 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 toolbar is shown THE toolbar SHALL draw itself for the new direction at once, without a motion of its own, keeping its focus and the item its Tab stop stands on. |
TBR-16 | WHILE variant is floating THE toolbar SHALL lay its items comp.toolbar.container.gap.floating apart (and WHILE it is inline comp.toolbar.container.gap.inline apart, TBR-17), measured between their visible bounds along its axis, and SHALL move two neighbours further apart only as far as each needs in order to keep, centred on itself, a length of sys.target.touch along the axis that its neighbour's does not reach into, because items that stand this close cannot extend a hit area over each other (as BGR-3): where the token is 0, as in the default system, an item at least sys.target.touch long touches its neighbour, and a shorter one stands half of what it lacks away from it. THE toolbar SHALL NOT widen, resize or restyle an item to do so (TBR-9), SHALL space its items the same whatever the input type, so that the toolbar does not change with the pointer, and SHALL space them so along the vertical axis WHILE orientation is vertical (TBR-6). A floating toolbar is as long as its items need (TBR-4) and never spreads them across a width; WHILE variant is docked THE toolbar SHALL spread its items across its width and keep comp.toolbar.container.gap.docked as the least space between them (TBR-2), unchanged. |
TBR-17 | WHILE variant is inline THE toolbar SHALL lay its items out in one line along its axis where the layout places it, as long as its items need and never longer than the layout allows, with comp.toolbar.container.padding-inline.inline at each end, comp.toolbar.container.gap.inline between neighbours spaced as TBR-16 spaces a floating toolbar's, and at least comp.toolbar.container.size.inline across its axis; it SHALL fill its container with comp.toolbar.container.color.inline, raise it by comp.toolbar.container.elevation.inline, round it by comp.toolbar.container.shape.inline and edge it comp.toolbar.outline.width thick in comp.toolbar.outline.color.inline (transparent, none and no edge in the default system), and SHALL NOT read a system inset, position itself or spread its items. |
TBR-18 | WHILE overflow is menu and the items, at their own sizes and with the least gaps, need more length along the axis than the layout offers the toolbar — the length it would take with every item shown, which a parent as long as its content passes on from further up, so that it can grow while the toolbar and its parent stay as long as the items still shown — THE toolbar SHALL hide items from its end, in reverse reading order and one at a time, skipping every item whose overflow is never, until the items that remain and the overflow button fit; SHALL draw the overflow button after the last item that remains — an icon-only Tanrei Button of the text variant and the medium size, showing the glyph more_horiz in a horizontal toolbar and more_vert in a vertical one, named by overflowLabel or the product's words, exposed as a button that opens a menu, with its expanded state; and SHALL NOT hide an item whose overflow is never even when the items then do not fit, in which case TBR-8 applies to what remains. |
TBR-19 | THE toolbar SHALL decide which items are hidden before the first frame in which it is drawn, and again in the frame in which its items or the length the layout offers it (TBR-18) change, so that no item is drawn and then hidden in a later frame, and its size across its axis never changes because an item was hidden or shown; hiding and showing items SHALL NOT be animated, because it answers a change of the layout that a motion would lag behind. |
TBR-20 | WHEN the overflow button is activated THE toolbar SHALL open a Tanrei Menu anchored to it, placed bottom-end (in a vertical toolbar, at the side away from the edge it stands at), holding the hidden items in reading order: an action or a labelled Button as a menu item with its label and its icon, disabled while it is; a toggle as a checkbox item with its selected state; a menu button as a submenu item holding its menu's items; a Button with menu as an item for its main action followed by a submenu item named by its menuLabel; and a separator, or the boundary between two groups, as the menu's separation. Activating a menu item SHALL do exactly what activating the hidden item does — the same click or change, once, from that item — and close the menu. |
TBR-21 | WHEN the item that is the toolbar's Tab stop, or has keyboard focus, is hidden THE toolbar SHALL make the overflow button the Tab stop and, if focus was on the item, move focus to the overflow button; THE overflow button SHALL be the last stop of the arrow-key order (TBR-10). |
TBR-22 | THE toolbar SHALL lay out a group's items in its own line, in place, as items of the toolbar (the group draws nothing of its own), SHALL expose a group whose label is not empty as a group named by that label, and a group whose label is empty as nothing (its items are the toolbar's), unless the product names it (TBR-26). |
TBR-23 | THE toolbar SHALL set two neighbouring groups — or a group and an item outside any group — apart as separation names: WHILE it is divider by a line comp.toolbar.divider.thickness thick and comp.toolbar.divider.length long across the axis, centred, in comp.toolbar.divider.color, with comp.toolbar.group.gap on each side; WHILE it is gap by twice comp.toolbar.group.gap of empty space. A separator item SHALL be drawn the same way wherever it stands. THE toolbar SHALL expose each such line as a separator, SHALL never make it a stop of the arrow keys, and SHALL draw no line at the start or the end of the toolbar, and never two lines side by side. |
TBR-24 | WHILE an item is the anchor of a Menu THE toolbar SHALL keep it an item of the arrow-key order and SHALL neither lay out nor count the Menu; WHILE that Menu is open its keys SHALL be the Menu's, and WHEN it closes focus SHALL return to its anchor, which stays the Tab stop. |
TBR-25 | THE toolbar SHALL make each half of a Button with menu a stop of its own in the arrow-key order, the leading half first in reading order, each keeping its own name, role and state. |
TBR-26 | WHERE the platform has ARIA attributes (the web), the toolbar's host and a group's host carries its role (TBR-11, TBR-22) itself, so what the product writes on it is not moved, but for a name: WHILE label is not empty THE toolbar SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is its whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE label is empty a name the product gives so — aria-label, or aria-labelledby naming an element — SHALL be its name and SHALL NOT be reported as having none in debug builds (TBR-12). A group whose label is empty and that the product names so SHALL be exposed as a group named by that name, as one with a label is: a group the product names is a named group, and a name on an element with no role would name nothing (TBR-22). WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaLabelledByElements) to null, THE toolbar SHALL take it away. |
プラットフォーム別の名前対応
パリティは API が同一であることを意味しません。同じ契約を、各プラットフォームの流儀で表現します。
| 仕様 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| コンポーネント | tanrei-toolbar | TanreiToolbar | TanreiToolbar | TanreiToolbar | Toolbar |
variant · プロパティ | variant | variant | variant | variant | variant |
orientation · プロパティ | orientation | orientation | orientation | orientation | orientation |
label · プロパティ | label | label | contentDescription | semanticLabel | accessibilityLabel |
overflow · プロパティ | overflow | overflow | overflow | overflow | overflow |
overflowLabel · プロパティ | overflowLabel | overflowLabel | overflowContentDescription | overflowSemanticLabel | overflowAccessibilityLabel |
separation · プロパティ | separation | separation | separation | separation | separation |
| 備考 | The items are <tanrei-button> children, icon-only and labelled, <tanrei-button-with-menu> children, <tanrei-toolbar-group> children holding them, and <tanrei-divider> separators; a <tanrei-menu anchor> child anchored to an item's id is that item's Menu and is not laid out. The host has role="toolbar", aria-orientation and aria-label from label; a group with a label has role="group" and aria-label. The toolbar assigns each of its children, and a group each of its items, to a slot of its own (manual slot assignment), so that the toolbar draws the separations between them and leaves a hidden item unassigned: a hidden item is not drawn and keeps its place, its listeners and its state in the page. The overflow button and its menu are elements of the toolbar's shadow root. The roving Tab stop sets tabindex on the item elements (0 on the Tab stop, -1 on the others): an item delegates focus to the button in its shadow root, and a host whose tabindex is -1 takes that button out of the Tab order; a Button with menu is told which of its halves is the Tab stop through the internal channel (SPB-33). A docked toolbar's bottom inset is env(safe-area-inset-bottom). Snapshot harness: the stages of TBR-1, TBR-4, TBR-6 and TBR-18. | TanreiToolbar(_ label: String, variant:, orientation:) { items }, a view builder of TanreiButtons, icon-only and labelled. A docked toolbar is placed with .safeAreaInset(edge: .bottom), its background ignoring the bottom safe area. SwiftUI's own .toolbar with a bottom-bar placement is not used: its bar cannot take the tokens. SwiftUI has no toolbar role: the toolbar is an accessibility container (.accessibilityElement(children: .contain)) labelled by label. The roving Tab stop is one focus group for the keyboard: Full Keyboard Access moves among its items with the arrow keys and leaves it with Tab. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. .toolbarOverflow(.never) on an item keeps it; the overflow menu is a SwiftUI Menu drawn from the Tanrei Menu's tokens. | TanreiToolbar(contentDescription, modifier, variant, orientation, windowInsets) { … }, the items called in its content lambda. windowInsets defaults to WindowInsets.navigationBars for a docked toolbar and to none for a floating one. Compose has no toolbar role: the toolbar is a traversal group (isTraversalGroup) labelled by contentDescription, and the README says so. The roving Tab stop is TanreiRovingFocus, the primitive the list, menu and tabs use, horizontal or vertical by orientation; the toolbar registers each item it holds. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. Modifier.toolbarOverflow(TanreiToolbarOverflow.Never) on an item keeps it; the overflow menu is a TanreiMenu. | TanreiToolbar(semanticLabel:, variant:, orientation:, children: [...]) (orientation is Flutter's Axis). A docked toolbar applies the bottom MediaQuery.padding itself, as SafeArea would, with its color under it. Flutter's widgets library has no toolbar role: the toolbar is a Semantics(container: true, explicitChildNodes: true, label:), and the README says so. The roving Tab stop is TanreiItemGroupMixin, the primitive the list, menu and tabs use, with its item axis set by orientation. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. An item wrapped in TanreiToolbarItem(overflow: TanreiToolbarOverflow.never, child:) is kept; the overflow menu is a TanreiMenu. | <Toolbar variant orientation accessibilityLabel insets>{items}</Toolbar>, the items being Button elements, icon-only and labelled. The view has role="toolbar". A docked toolbar takes the bottom inset from its own insets prop, falling back to TanreiProvider's insets; the package takes no safe-area dependency. The roving Tab stop and the arrow keys work where a hardware keyboard is attached, as the tabs row's do. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. toolbarOverflow="never" on an item keeps it; the overflow menu is a Menu. |
トークン
| トークン | 参照先 | 解決値(ライト) |
|---|---|---|
--tanrei-comp-toolbar-container-color-docked | sys.color.surface | |
--tanrei-comp-toolbar-container-color-floating | sys.color.surface-bright | |
--tanrei-comp-toolbar-container-color-inline | — | |
--tanrei-comp-toolbar-container-elevation-docked | sys.elevation.level0 | |
--tanrei-comp-toolbar-container-elevation-floating | sys.elevation.level3 | 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06) |
--tanrei-comp-toolbar-container-elevation-inline | sys.elevation.level0 | |
--tanrei-comp-toolbar-container-gap-docked | — | 16px |
--tanrei-comp-toolbar-container-gap-floating | sys.space.space0 | 0px |
--tanrei-comp-toolbar-container-gap-inline | sys.space.space50 | 4px |
--tanrei-comp-toolbar-container-padding-inline-docked | sys.space.space200 | 16px |
--tanrei-comp-toolbar-container-padding-inline-floating | sys.space.space150 | 12px |
--tanrei-comp-toolbar-container-padding-inline-inline | sys.space.space0 | 0px |
--tanrei-comp-toolbar-container-shape-docked | sys.shape.corner.none | 0px |
--tanrei-comp-toolbar-container-shape-floating | sys.shape.corner.full | 9999px |
--tanrei-comp-toolbar-container-shape-inline | sys.shape.corner.medium | 8px |
--tanrei-comp-toolbar-container-size-docked | — | 64px |
--tanrei-comp-toolbar-container-size-floating | — | 64px |
--tanrei-comp-toolbar-container-size-inline | — | 40px |
--tanrei-comp-toolbar-divider-color | sys.color.outline-variant | |
--tanrei-comp-toolbar-divider-length | sys.space.space300 | 24px |
--tanrei-comp-toolbar-divider-thickness | sys.border.hairline | 1px |
--tanrei-comp-toolbar-group-gap | sys.space.space100 | 8px |
--tanrei-comp-toolbar-outline-color-docked | sys.color.outline-variant | |
--tanrei-comp-toolbar-outline-color-floating | — | |
--tanrei-comp-toolbar-outline-color-inline | — | |
--tanrei-comp-toolbar-outline-width | sys.border.hairline | 1px |
--tanrei-comp-toolbar-separation | sys.behavior.separation | 0 |
フィクスチャ
基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。
variant: inline (8)
inline-formatting
label: Formatting overflowLabel: More formatting items: 4 inline-separation-gap
label: Formatting overflowLabel: More formatting separation: gap items: 2 inline-overflow
label: Message actions overflowLabel: More actions items: 8 inline-overflow-pinned
label: Compose overflowLabel: More actions items: 6 inline-menu-button
label: Table actions overflowLabel: More table actions items: 3 inline-button-with-menu
label: Document actions overflowLabel: More document actions items: 4 inline-labelled-toggles
label: Filters overflowLabel: More filters items: 3 rtl-inline-overflow
label: Message actions overflowLabel: More actions direction: rtl items: 8 variant: docked (4)
docked
variant: docked label: Message actions items: 4 docked-button
variant: docked label: Compose items: 4 docked-one-item
variant: docked label: Reply items: 1 docked-disabled-item
variant: docked label: Message actions items: 4 variant: floating (4)
floating
variant: floating label: Formatting items: 4 floating-filled-action
variant: floating label: Drawing tools items: 4 floating-vertical
variant: floating orientation: vertical label: Map controls items: 3 floating-vertical-groups
variant: floating orientation: vertical label: Map controls overflowLabel: More map controls items: 2 生成元 specs/toolbar.spec.json, tokens/comp/toolbar.tokens.json, design/tokens/
目の前にあるものに働きかけるコントロールをまとめるときに使います。エディターの上の太字・斜体・リンク、表の上の絞り込み・並べ替え・ダウンロード、開いているメッセージのアーカイブ・削除・移動、絵の上のペンと消しゴム、地図の拡大・縮小・現在地などです。どのコントロールも、いまのコンテンツや選択中のものに働きかけるもので、次にどこへ行くかを選ぶものではありません。
画面のタイトル、戻る操作、画面全体に関わる 1〜2 個のアクションは、上端のアプリバーに置きます。アプリの最上位の行き先を切り替えるならナビゲーションバーを使います。ドックのツールバーとナビゲーションバーは同じ画面に置きません。どちらも画面の下端を使うからです。
| バリアント | 置かれる場所 | 項目の並び | 用途 |
|---|---|---|---|
| インライン(既定) | コンテンツの中の、利用者側のレイアウトが置いた場所。既定のシステムでは自分のサーフェスを持たない | 間を詰めて並べ、必要な長さだけになる。各項目は自分のタッチターゲットの分の余裕を保つ | ページの一部分のコントロール。エディターの書式、表のアクション、絞り込みの並びなど |
| ドック | 画面の下端いっぱいに、下端に接して。色はシステムバーの下まで届く | 幅いっぱいに、等間隔で広げる | スマートフォンの詳細画面のアクション。開いているメッセージ、写真、ドキュメントなど |
| フローティング | コンテンツの上に浮く、角の丸いサーフェス。置き場所は利用者側のレイアウトが決める | 間を詰めて並べ、必要な長さだけになる。各項目は自分のタッチターゲットの分の余裕を保つ | 周りのコンテンツを見せたまま使う道具。キャンバス、地図など |
ドックのツールバーは常に横向きです。インラインとフローティングは縦向きにもでき、コンテンツの横に置けます。1 つのプロダクトの中で、ある画面ではインラインに置き、別の画面ではドックにし、さらに別の画面では浮かせてかまいません。デザインシステムが追加したバリアントは、自分の値を使いつつ、インラインの形で並びます。
ドックのツールバーは、項目を幅いっぱいに広げ、隙間(既定のシステムでは 16)より近づけることはありません。インラインとフローティングのツールバーは項目の間を詰めます。隣り合う 2 つの項目は、それぞれが並びの方向に自分のタッチターゲットを保つのに必要な分だけ離れ、隙間(既定のシステムではインラインで 4、フローティングで 0)より近づくことはありません。large のボタンどうしは接し、medium では 8、small では 16 離れます。このためにツールバーが項目の幅を広げたり見た目を変えたりすることはなく、間隔はマウスでもタッチでも同じです。
フローティング:small、medium、large の項目と、narrow と wide の項目
- ほとんどのアクションには、
textバリアントのアイコンだけのボタンを使います。ツールバーの行き着く先になるアクション(送信、完了など)は 1 つだけ目立たせます。Filled のアイコンだけのボタン(幅をwideにしてもかまいません)にするか、アイコンより言葉のほうが伝わるなら、ラベルのある Filled のボタンにします。 - 小さなアクションがたくさん並ぶツールバーには、
narrowのアイコンだけのボタンが向いています。見た目はターゲットより狭くなりますが、ツールバーが隣との間にターゲットの分の余裕を保ちます。 - 太字やミュートのようにオンとオフを切り替えるアクションは、
toggleを付けたアイコンだけのボタンにします。オンとオフのある絞り込みは、toggleを付けたラベルのあるボタンにします。選択状態も読み上げも、それぞれのボタン自身が持ちます。 - いくつかのコマンドから選ばせるコントロールには、ツールバーの中のボタンにメニューをアンカーします。保存と名前を付けて保存のように、バリエーションのあるメインのアクションにはメニュー付きボタンを使います。
- 並び順は変えないようにします。人はアクションを位置で覚えるので、画面ごとに項目の位置が変わると覚えにくくなります。
- 項目が 1 つだけのドックのツールバーは、その項目を中央に置きます。その項目はコンテンツの中に置いたほうがよくないか、一度考えてみてください。
関連する項目はグループにまとめます。太字・斜体・下線、元に戻す・やり直す、拡大・縮小などです。ツールバーは、2 つのグループの間と、グループとどのグループにも入っていない項目の間を、デザインシステムがほかの場所でパーツを区切るのと同じやり方で区切ります。既定のシステムでは短い線とその両側の 8 の余白で、区切りを余白で表すデザインシステムでは、線のない 16 の空きで区切ります。ツールバーの separation は、そのツールバーに固有の理由があるときだけ指定します。区切りの項目は、グループに入っていない 2 つの項目の間に同じ線を引きます。
ツールバーは両端に線を引くことはなく、2 本の線を並べることもありません。まとまりがその人にとって意味を持つとき(「文字のスタイル」)はグループに label を付け、スクリーンリーダーがその名前を伝えるようにします。ラベルのないグループは、項目を区切るだけで何も伝えません。
ツールバーは項目を折り返したり縮めたりしません。項目が使える長さより多くの場所を必要とするときは、末尾から 1 つずつ項目を隠し、残った最後の項目の後ろにオーバーフローボタンを表示します。このボタンは、隠れた項目を元の順に、ラベル、アイコン、状態とともに並べたメニューを開きます。アクションはメニュー項目に、トグルはチェックボックスの項目に、メニューボタンはサブメニューに、メニュー付きボタンはメインのアクションとそれに続くサブメニューになります。どれを選んでも、隠れた項目とまったく同じことをします。オーバーフローボタンの名前は overflow-label(「その他のアクション」)で付けるか、プロダクト全体で一度だけ setWords({ toolbarOverflowLabel: … }) で付けます。
送信や完了のように残さなければならない項目には toolbar-overflow="never" を付けます。ツールバーは代わりにそれより前の項目を隠します。隠さない項目だけでもなお収まらないときは、列の残りがスクロールします。どの項目も隠さずにすべてを列に残し、スクロールさせるなら、overflow="scroll" を指定します。
何を隠すかはツールバーを描く前に決まり、長さが変わったときもそのフレームの中で決め直します。そのため、いったん描かれた項目が後から隠れることはなく、列が跳ねることもありません。
- ツールバーには、中のコントロールが何をするものかを表す名前を付けます。「書式」「メッセージのアクション」などです。
- いま使えないアクションは、取り除かずに無効にします。そうすればほかの項目の位置が動きません。
- よく使われる項目ほど先頭に置きます。ツールバーは末尾から隠すからです。
- フローティングのツールバーは、使っている間も見えている必要があるコンテンツや、ナビゲーションバー、システムのインセットにかからない場所に置きます。
- テキストフィールド、スライダー、チップの並びをツールバーに入れない。どれも、ツールバーがフォーカスの移動に使う矢印キーを必要とします。検索はアプリバーかページに置きます。
- ドックのツールバーとナビゲーションバーを同じ画面に置かない。
- ツールバーを目立たせるために項目の見た目を変えない。代わりに、ボタンのバリアントを Tonal か Filled にします。
- ナビゲーションの行き先をツールバーに入れない。ツールバーの項目は、コンテンツに働きかけるコントロールです。
- ロール
toolbar- ラベル付け
- label names the toolbar; it is never drawn. A group's label names the group; empty leaves it unexposed. Each item is named by its own label (an icon-only Button's label, a labelled Button's text), and the overflow button by overflowLabel or the product's words. The toolbar adds no name, role or state to its items.
- 最小ターゲット
- 見た目のサイズが小さくても、ヒット領域は最小ターゲットサイズを満たします。 minimum 32px · pointer 44px · spacing 8px · touch 48px
キーボード操作
| キー | 動作 |
|---|---|
| Tab / Shift+Tab | Moves focus into the toolbar (to the item focused last, or the first enabled item) and out of it: the toolbar is one Tab stop. |
| Right Arrow / Left Arrow (horizontal), Down Arrow / Up Arrow (vertical) | Moves focus to the next or previous enabled item — a group's items and each half of a Button with menu included, separators skipped, the overflow button last — wrapping at the ends (Left and Right mirrored in right-to-left layouts). |
| Home / End | Moves focus to the first or last enabled item. |
| Enter / Space | Activates the focused item, as that Button does; on the overflow button, opens the menu of the hidden items. |
備考
- The toolbar has no target of its own and takes no input: its items are the product's Buttons, icon-only and labelled, which meet the minimum target themselves (BTN-5). A medium item's target (48) fits inside a floating toolbar's 64, and reaches past an inline toolbar's 40 as a Button's does anywhere.
- The web has the toolbar role (with aria-orientation) and React Native has role toolbar. SwiftUI, Compose and Flutter have no toolbar role: there the toolbar is a group (an accessibility container, a traversal group, a semantics container) named by label, and each README says so.
- One Tab stop with arrow-key movement is the WAI-ARIA toolbar pattern. It uses the same roving focus the list, menu and tabs use on each platform; a Button outside a toolbar is unchanged.
- A toolbar holds Buttons, menu buttons, Buttons with menu, groups of them and separators. Anything else is laid out and reported (TBR-12), because a control that uses the arrow keys would take the keys the toolbar moves focus with.
- A docked toolbar and a navigation bar are not used on the same screen: both claim the bottom edge.
- How the items look is their own design system's answer; the toolbar reads no state tokens.
- Hidden items stay reachable: the overflow button is the last stop of the arrow keys, and its menu holds every hidden item with its own name and state; activating one is activating the item.
生成元 specs/toolbar.spec.json, tokens/sys/measure.tokens.json
- WAI-ARIA のツールバーパターンに沿い、Tab で止まるのはツールバーの中の 1 か所だけです。Tab で入ると、最後にキーボードフォーカスのあった項目、それもなければ最初の無効でない項目に止まり、次の Tab でツールバーから出ます。トグルが 8 個並ぶ書式ツールバーも、Tab 1 回で通り過ぎられます。
- 中では、ツールバーの向きに沿った矢印キーで次または前の項目へ移り、端では折り返します。横向きなら右矢印と左矢印、縦向きなら下矢印と上矢印です。Home と End で最初と最後の項目へ移ります。右から左(RTL)のレイアウトでは、左矢印で次の項目へ移ります。グループの項目はツールバーの項目として扱い、メニュー付きボタンは左右それぞれが止まる位置になり、区切りは飛ばし、オーバーフローボタンが最後に止まる位置になります。
- フォーカスを動かしただけで項目が実行されることはありません。Enter と Space は、フォーカスのある項目にその項目自身の動きをさせるので、トグルはそのまま切り替わります。
- 項目にアンカーしたメニューが開いている間は、キー操作はメニューのものです。メニューが閉じると、フォーカスはそれを開いた項目に戻ります。
- ツールバーが狭くなってフォーカスのある項目が隠れたときは、フォーカスはオーバーフローボタンへ移ります。
- 無効な項目は矢印キーで飛ばされ、Tab で止まる位置にもなりません。無効なボタンはフォーカスを受け取らないからです。
- このフォーカス移動は、各プラットフォームでリスト、メニュー、タブが使っているものと同じ仕組みです。
- ツールバーのロールがあるプラットフォーム(Web、React Native)では、
labelを名前とし、向きを持つツールバーとして伝えます。SwiftUI、Compose、Flutter にはこのロールがないため、labelを名前とし、項目を順に含むグループとして伝えます。 labelのあるグループは、その名前を持つグループとして伝えます。グループの間の線は区切りとして伝えます。- オーバーフローボタンは、メニューを開くボタンとして、展開状態とともに伝えます。そのメニューには、隠れた項目がすべて、それぞれの名前と状態とともに入ります。
- 各項目は、自分の名前、ロール、状態、ターゲットをそのまま保ちます。ツールバーはそこに何も加えず、見た目も変えません。
- 隣り合う 2 つの項目のヒット領域は重なりません。ドックのツールバーでは隙間のちょうど中間で接します。既定のシステムの隙間(16)は、そこでもどの項目も 48 のタッチターゲット全体を保てる幅です。隙間を狭くしたデザインシステムでは、項目の間のターゲットも狭くなります。インラインとフローティングのツールバーでは、ポインターの種類にかかわらず、各項目が並びの方向にタッチターゲット 1 つ分の長さを保てるだけ離して並べるので、narrow や small のボタンが隣に押し狭められることはありません。隣がない側では、項目はターゲット全体を保ちます。
overflowがscrollのとき、キーボードフォーカスを受け取った項目がスクロールで見えない位置にあれば、全体が見えるところまでスクロールします。- 文字を大きくすると、ツールバーは向きと直交する方向に伸びて項目を収めます。項目そのものを拡大することはありません。
labelがない場合、項目がない場合、どの種類にも当てはまらない項目がある場合、ドックなのに縦向きが指定された場合、オーバーフローボタンの名前がない場合、バリアント、向き、overflow、separationに知らない値が指定された場合は、デバッグビルドで報告します。
プラットフォームごとの既知の制約:
- SwiftUI、Compose、Flutter にはツールバーのロールがありません。スクリーンリーダーには、ツールバーではなく名前の付いたグループとして聞こえます。各パッケージの README にもそう書いています。このグループは向きを伝えません。
- SwiftUI、Compose、Flutter、React Native は追従待ちです。インラインのバリアント、グループと区切り、オーバーフローメニューがなく、項目にはボタンしか入れられません。
- Web:ドックのツールバーは
env(safe-area-inset-bottom)を読みますが、この値はページの viewport メタにviewport-fit=coverがない限り 0 です。 - Web:縦向きのツールバーのオーバーフローメニューは、オーバーフローボタンの横ではなく下に開きます。メニューに横に開く配置がないためです。
- Web:インラインとフローティングのツールバーは、使える長さを親要素から測ります。flex の行の中で、親ではなく兄弟要素によって長さが決まるツールバーは、親か項目のサイズが変わったときにしか、何が収まるかを決め直しません。
- Web:項目がスクロールしている間、項目のうちツールバーの軸と直交する方向にはみ出す部分(ヒット領域、フォーカスリングやグロー、Elevated のボタンの影)は切り取られます。スクロールバーは表示しません。
- SwiftUI では、Tab で止まる 1 か所は、キーボード操作のための 1 つのフォーカスグループになります。フルキーボードアクセスでは、矢印キーで項目の間を移り、Tab でグループから出ます。キー操作は規則としてテストしているだけで、実際のキーボードではまだ確かめていません。
- SwiftUI:項目がスクロールしている間、端の項目のヒット領域はツールバーの端で止まります。サイズが変わって項目がスクロールし始めたり、スクロールしなくなったりすると、項目は作り直されるため、項目のキーボードフォーカスや押している途中の操作が失われます。
- React Native では、Tab で止まる 1 か所と矢印キーの移動は、タブの並びと同じく、ハードウェアキーボードをつないだときだけ働きます。
- React Native にはツールバーのロールはありますが、向きを伝える手段がありません。そのため、縦向きのツールバーも縦向きとは読み上げられません。
- React Native:フローティングのツールバーの幅、ハードウェアキーボードでのキー操作、フォーカスを受けた項目のスクロール、VoiceOver と TalkBack がツールバーを読み上げるかどうかは、Jest で確かめただけで、実機ではまだ確かめていません。
m3-likeでは、ツールバーは既定のシステムの値で描かれます。Material のツールバーの値は、忠実度の参照データがツールバーを扱うようになるまで保留しています。項目の色まで塗り替える Material の vibrant なツールバーは描けません。ツールバーが項目の見た目を変えないためです(mimicry-gaps 台帳toolbar-vibrant-color)。
- ツールバーに
labelを付け、アイコンだけのものも含めて、すべての項目にプロダクトの言語でラベルを付けます。オーバーフローボタンにも、overflow-labelかプロダクト側の言葉で名前を付けます。 - 入れるのは Tanrei のボタンとメニュー付きボタンだけにします。単独で置くか、グループに入れるか、メニューをアンカーして置きます。
- 使えないアクションは無効にし、理由が分かりにくいときは近くで理由を伝えます。
- メニューボタンのメニューは、ほかのメニューと同じく、利用者側で開閉します。
- フローティングのツールバーは、表示している間も必要なコンテンツを覆わない場所に置きます。