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

工具栏

把一组相关的控件作为一个 Tab 停靠点容纳在一起:内嵌在内容需要它的地方、停靠在底边,或悬浮在内容之上。方向键在它的各个控件之间移动,放不下的项会进入溢出菜单。

content_copy以 Markdown 格式查看
类别
操作
令牌
comp.toolbar
层级
基础

概览

Web
tanrei-toolbar
schedule
SwiftUI
TanreiToolbar
schedule
Jetpack Compose
TanreiToolbar
schedule
Flutter
TanreiToolbar
schedule
React Native
Toolbar
schedule

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

内嵌、停靠与悬浮

format_boldformat_italicformat_underlinedlinkimagearchivedeletemark_email_unreaddrive_file_moveeditink_highlighterink_erasercheck

用法

工具栏容纳一组相关的控件:操作、切换按钮、打开菜单的按钮、带菜单的按钮。它的项就是你自己的 Tanrei Button 和带菜单的按钮,各有自己的标签、点击处理和状态。工具栏按阅读顺序排列它们,把各组分隔开,在它们之间移动键盘焦点,并把放不下的项收进溢出菜单。它从不改变它们的样式,所以工具栏里的纯图标 Button,外观和行为与在其他任何地方相同。

它有六类项:

  • action:纯图标的 Button。“粗体”这样的切换按钮就是设置了 toggle 的 action。
  • button:带标签的 Button,也可以是切换按钮,例如一个开或关的筛选条件。
  • menu-button:锚定了 Tanrei 菜单的 Button,例如“排序”或“插入”。菜单是你自己的,放在它的 Button 旁边。工具栏不负责菜单的布局。
  • button-with-menu:一个主操作,加上一个放相关操作的菜单。两半各是方向键的一个停靠点。
  • group:一串相关的项,例如“粗体”“斜体”和“下划线”。它自己不绘制任何东西。当这种分组对用户有意义时,给它一个 label,例如“文字样式”。
  • separator:两个不在组里的项之间的一条线。在 Web 上它是一条分隔线,工具栏像绘制两组之间的线那样绘制它。

内嵌(默认)。 内嵌的工具栏位于你的布局放置它的地方,就在它所作用的内容之中:表格上方、编辑器顶部、列表旁边作为一排筛选条件。它的长度就是它的项所需要的长度,绝不超过布局允许的长度。在默认系统中,它没有自己的表面、阴影或边缘,两端与周围的内容对齐。

停靠。 停靠的工具栏横跨屏幕的底边。它的颜色会延伸到主屏幕指示条或手势条之下,它的项则留在其上方,并位于两侧的边衬区(insets)之内。Web 读取 env(safe-area-inset-bottom),SwiftUI 用 .safeAreaInset(edge: .bottom) 放置工具栏,Compose 接收 windowInsets(默认是导航栏),Flutter 读取 MediaQuery 底部的 padding。React Native 从工具栏的 insets 属性或从 TanreiProvider 获得边衬区,因为 Tanrei 不引入安全区域方面的依赖。

悬浮。 悬浮的工具栏不会自己定位,主操作按钮也是这样。由你的布局来放置它:通常居中放在内容底部的上方;纵向时则放在内容末尾边的旁边。

Web 和 React 的示例展示的是当前规格。SwiftUI、Compose、Flutter 和 React Native 待跟进:它们提供的仍是工具栏和按钮的上一版。在上一版中,除非另行指定,工具栏是停靠的,只容纳 Button,放不下的项会滚动,纯图标的操作是一个独立的图标按钮。在跟上之前,这些平台的示例展示的是上一版。

<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

// 停靠的工具栏位于底部安全区域 inset 中;它的背景会延伸到 inset 之下。
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>

规格

组与分隔符、菜单按钮、带菜单的按钮、带标签的切换按钮、溢出菜单

undoredosort未关闭分配给我有附件archivedeletemark_email_unreaddrive_file_movelabelflagsnoozeprintattach_fileimagelink发送addremovemy_locationlayers

构成

部件 说明
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 #ffffff
--tanrei-comp-toolbar-container-color-floating sys.color.surface-bright #ffffff
--tanrei-comp-toolbar-container-color-inline — #000000 · 0%
--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 #000000 · 8%
--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 #000000 · 8%
--tanrei-comp-toolbar-outline-color-floating — #000000 · 0%
--tanrei-comp-toolbar-outline-color-inline — #000000 · 0%
--tanrei-comp-toolbar-outline-width sys.border.hairline 1px
--tanrei-comp-toolbar-separation sys.behavior.separation 0

测试样例

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

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

指南

何时使用

工具栏用来放置一组作用于用户眼前内容的控件:编辑器上方的“粗体”“斜体”和“链接”;表格上方的“筛选”“排序”和“下载”;一封打开的邮件的“归档”“删除”和“移动”;绘图上方的画笔和橡皮擦;地图上的缩放和定位。这些控件作用于当前内容或当前选择,而不决定用户接下来去哪里。

屏幕的标题、返回入口以及一两个全局操作,请使用顶部的应用栏。要在应用的各个顶级目的地之间切换,请使用导航栏。停靠的工具栏和导航栏绝不出现在同一个屏幕上:两者都要占用底边。

内嵌、停靠还是悬浮

变体 所在位置 项 适用于
Inline(默认) 就在内容之中,由你的布局放置;在默认系统中没有自己的表面 彼此靠拢,需要多长就多长,每个项都为自己的触摸目标留出空间 页面某一部分的控件:编辑器的格式、表格的操作、一排筛选条件
Docked 横跨底边并与之齐平,颜色延伸到系统栏之下 沿宽度铺开,彼此间距相等 手机上详情屏幕的操作:一封打开的邮件、一张照片、一份文档
Floating 内容之上一块圆角、抬升的表面,由你的布局放置 彼此靠拢,需要多长就多长,每个项都为自己的触摸目标留出空间 位于内容之上的工具,而周围的内容应保持可见:画布、地图

停靠的工具栏始终是横向的。内嵌或悬浮的工具栏可以是纵向的,位于内容旁边。同一个产品可以在一个屏幕上内嵌工具栏,在另一个屏幕上停靠工具栏,在第三个屏幕上让工具栏悬浮。设计系统增加的变体采用内嵌的结构,并使用自己的值。

停靠的工具栏把它的项沿宽度铺开,项与项之间的距离绝不小于它的间隙(默认系统中为 16)。内嵌或悬浮的工具栏让它的项彼此靠拢:相邻两项之间隔开的距离,只够让每个项沿这一排保有自己的触摸目标,且绝不小于间隙(默认系统中内嵌为 4,悬浮为 0)。大号按钮彼此相接,中号按钮相隔 8,小号按钮相隔 16。工具栏绝不会为此把某个项加宽或改变它的样式,而且无论用鼠标还是触摸,间距都相同。

悬浮:small、medium 和 large 的项,以及 narrow 和 wide 的项

format_boldformat_italicformat_underlinedformat_boldformat_italicformat_underlinedformat_boldformat_italicformat_underlinedundoredoeditink_erasercheck

项

  • 大多数操作使用 text 变体的纯图标按钮。工具栏最终要引向的那个操作(例如“发送”或“完成”)应当更醒目:用 filled 的纯图标按钮,也可以配上 wide 宽度;如果文字比图标更容易理解,就用带标签的 filled 按钮。
  • narrow 的纯图标按钮适合有许多小操作的工具栏。它画出来比它的目标窄,工具栏会在它与相邻项之间为这个目标留出空间。
  • 用来开启和关闭某项功能的操作(“粗体”“静音”),做成带 toggle 的纯图标按钮。开或关的筛选条件,做成带 toggle 的带标签按钮。每个按钮都保有自己的选中状态和自己的朗读内容。
  • 对于提供几条命令供选择的控件,把一个菜单锚定到工具栏里的某个 Button 上。对于有若干变化形式的主操作,例如“保存”和“另存为”,请使用带菜单的按钮。
  • 保持顺序稳定。用户靠位置寻找操作,各个屏幕上项的位置变来变去的工具栏很难学会。
  • 只有一个项的停靠工具栏会把它居中。请考虑一下,这个项是不是更应该放在内容里。

组与分隔符

把相关的项放进一个组:“粗体”“斜体”和“下划线”;“撤销”和“重做”;“放大”和“缩小”。工具栏会把两个组分隔开,也会把一个组与不属于任何组的项分隔开,方式与设计系统在别处分隔各部分时相同:在默认系统中是一条短线,两侧各留 8 的空间;在以空间作为分隔方式的设计系统下,则是 16 的空白,没有线。只有当某个工具栏有它自己的理由时,才在它上面设置 separation。分隔符项在两个不在组里的项之间绘制同样的线。

工具栏绝不在任何一端画线,也绝不让两条线并排出现。当分组对用户有意义时(“文字样式”),给组一个 label,让屏幕阅读器读出它的名称。没有标签的组只把它的项分隔开,什么也不朗读。

项放不下时

工具栏绝不会让项换行或缩小。当它的项需要的空间超过它所拥有的,它会从末尾开始一次隐藏一个项,并在剩下的最后一个项之后显示一个溢出按钮。这个按钮打开一个菜单,按原有顺序列出被隐藏的项,带着它们的标签、图标和状态:操作成为菜单项,切换按钮成为复选项,菜单按钮成为子菜单,带菜单的按钮成为它的主操作,后面跟着一个子菜单。选择其中一项,所做的事与被隐藏的项完全相同。用 overflow-label(“更多操作”)为溢出按钮命名,或者用 setWords({ toolbarOverflowLabel: … }) 为整个产品命名一次。

用 toolbar-overflow="never" 标记必须留下的项,例如“发送”或“完成”:工具栏会改为隐藏它前面的项。如果绝不隐藏的项仍然放不下,这一排其余的部分会滚动。要让每个项都留在这一排里、改为滚动而不隐藏任何项,请设置 overflow="scroll"。

隐藏哪些项,在工具栏绘制之前就已决定,并在它的长度变化的那一帧里重新决定,所以绝不会出现某个项先被画出来又被隐藏的情况,这一排也绝不会跳动。

推荐

  • 按工具栏中的控件所做的事来给它命名:“格式”“邮件操作”。
  • 对眼下不适用的操作,请禁用它而不是移除它,这样其他操作就不会移位。
  • 排列各项时,把用户最需要的放在前面:工具栏从末尾开始隐藏。
  • 放置悬浮的工具栏时,避开用户在使用它时需要看到的内容,也避开导航栏和系统边衬区。

避免

  • 不要在工具栏里放文本字段、滑块或一组纸片(Chip)。它们各自都需要方向键,而工具栏正是用方向键来移动焦点的。搜索应当放在应用栏或页面里。
  • 不要在同一个屏幕上同时使用停靠的工具栏和导航栏。
  • 不要为了让工具栏显得突出而改变项的样式。请改为给按钮选用 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

组件负责的部分

  • 工具栏是一个 Tab 停靠点,遵循 WAI-ARIA 的 toolbar 模式。Tab 进入工具栏时,落在上次拥有键盘焦点的项上,否则落在第一个未禁用的项上;再按一次 Tab 就离开工具栏。一个有八个切换按钮的格式工具栏,按一次 Tab 就能越过,而不是八次。
  • 在工具栏内部,沿其轴向的方向键移动到下一项或上一项,并在两端循环:横向工具栏用右、左方向键,纵向工具栏用下、上方向键。Home 和 End 移动到第一项和最后一项。在从右到左的布局中,左方向键移动到下一项。组里的项就是工具栏的项,带菜单的按钮的两半各是一个停靠点,分隔符会被跳过,溢出按钮是最后一个停靠点。
  • 移动焦点绝不会激活某个项。Enter 和 Space 对获得焦点的项起作用,效果与该项自身的行为一致,所以切换按钮照常切换。
  • 锚定在某个项上的菜单打开期间,按键归菜单处理。菜单关闭时,焦点回到打开它的那个项。
  • 当获得焦点的项因工具栏变窄而被隐藏时,焦点移到溢出按钮上。
  • 方向键会跳过禁用的项,禁用的项也永远不会成为 Tab 停靠点,因为禁用的按钮不可聚焦。
  • 这里的游动焦点与列表、菜单和标签页在各平台上使用的是同一套。
  • 在有 toolbar 角色的平台(Web、React Native)上,工具栏暴露为一个由 label 命名的 toolbar,并带有它的方向。SwiftUI、Compose 和 Flutter 没有这个角色:在这些平台上,它是一个由 label 命名的组,按顺序包含它的各个项。
  • 带 label 的组暴露为一个以该名称命名的组。组与组之间的线暴露为分隔符。
  • 溢出按钮暴露为一个打开菜单的按钮,带有它的展开状态;它的菜单包含每一个被隐藏的项,各带自己的名称和状态。
  • 每个项保留自己的名称、角色、状态和目标。工具栏不给它们添加任何东西,也从不改变它们的样式。
  • 相邻两项的点击区域绝不重叠。在停靠的工具栏中,它们在间隙的正中相接;默认系统的间隙(16)足够宽,每个项在那里同样能保有完整的 48 触摸目标;间隙更窄的设计系统会让项与项之间的目标变窄。在内嵌或悬浮的工具栏中,无论使用哪种指针,各项之间的距离都恰好够每个项沿这一排保有一个触摸目标的长度,所以窄的或小的按钮绝不会被相邻项挤占。在没有相邻项的一侧,项保有完整的目标。
  • 当 overflow 为 scroll 时,获得键盘焦点的项如果已滚出可见范围,工具栏会把它完整地滚入可见范围。
  • 文本变大时,工具栏沿垂直于其轴向的方向变大,以容纳它的项。它不会缩放这些项。
  • 调试构建会报告:缺少 label、工具栏没有任何项、某个项不属于任何一类、停靠的工具栏被设为纵向、溢出按钮缺少名称,以及未知的变体、方向、overflow 或 separation。

已知的平台差距:

  • SwiftUI、Compose 和 Flutter 没有 toolbar 角色。屏幕阅读器读到的是一个有名称的组,而不是工具栏,各个包的 README 都说明了这一点。这个组不暴露它的方向。
  • SwiftUI、Compose、Flutter 和 React Native 待跟进:它们没有内嵌变体,没有组或分隔符,没有溢出菜单,而且只接受 Button 作为项。
  • Web:停靠的工具栏读取 env(safe-area-inset-bottom),除非页面的 viewport meta 带有 viewport-fit=cover,否则它的值是零。
  • Web:纵向工具栏的溢出菜单在溢出按钮下方打开,而不是在它旁边,因为菜单没有侧向的放置方式。
  • Web:内嵌或悬浮的工具栏从它的父元素测量自己可以占用的空间。如果在一个 flex 行中,工具栏的空间由它的兄弟元素而不是父元素决定,那么只有当它的父元素或它的项改变尺寸时,它才会重新决定哪些项放得下。
  • Web:项在滚动时,某个项在垂直于工具栏轴向的方向上超出工具栏的部分都会被裁掉:它的点击区域、它的焦点环或光晕、elevated 按钮的阴影。不显示滚动条。
  • 在 SwiftUI 上,这一个 Tab 停靠点是面向键盘的焦点组:全键盘控制(Full Keyboard Access)用方向键在它的各项之间移动,用 Tab 离开它。这些按键是作为规则测试的,尚未用真实键盘验证。
  • SwiftUI:项在滚动时,末端项的点击区域止于工具栏的边缘。尺寸变化使这些项开始滚动或不再滚动时,它们会被重新创建,所以项会失去键盘焦点,或中断正在进行的按下。
  • 在 React Native 上,Tab 停靠点和方向键只在连接了硬件键盘时才起作用,与标签页那一排的情况相同。
  • React Native 有 toolbar 角色,但没有办法暴露方向,所以纵向的工具栏不会朗读为纵向。
  • React Native:悬浮工具栏的宽度、硬件键盘上的按键、把获得焦点的项滚入可见范围,以及 VoiceOver 和 TalkBack 是否朗读工具栏,都只在 Jest 中验证过,没有在真机上验证。
  • 在 m3-like 下,工具栏绘制的是默认系统的值:Material 的工具栏取值要等保真度参考覆盖到它。Material 的 vibrant 工具栏会给它的项改色,这无法绘制,因为工具栏从不改变项的样式(模仿差距 toolbar-vibrant-color)。

需要你做的事

  • 给工具栏一个 label,用你的产品语言给每个项各自的标签(纯图标的操作也不例外),并用 overflow-label 或你产品自己的文字给溢出按钮一个名称。
  • 里面只放 Tanrei Button 和带菜单的按钮,可以单独放、放进组里,或给它们锚定菜单。
  • 操作不适用时,请禁用它;原因不明显时,在附近说明原因。
  • 菜单按钮的菜单由你自己打开和关闭,就像对待任何菜单一样。
  • 放置悬浮的工具栏时,不要让它盖住工具栏打开期间仍然需要的内容。