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

导航栏

位于 compact 屏幕的底边,用来在三到五个顶级目的地之间切换,并标明当前所在的是哪一个。

content_copy以 Markdown 格式查看
类别
导航
令牌
comp.navigation-bar
层级
基础

概览

Web
tanrei-navigation-bar
schedule
SwiftUI
TanreiNavigationBar
schedule
Jetpack Compose
TanreiNavigationBar
schedule
Flutter
TanreiNavigationBar
schedule
React Native
NavigationBar
schedule
不提供的变体
cupertino: filled · m3-expressive-like: filled · m3-like: filled

生成自 docs/status/status.json, specs/navigation-bar.spec.json

规格已修订,尚未覆盖所有平台

本页描述的是规格当前所定的导航栏,它以标签页为样板。活动指示器是当前目的地在导航栏中平分到的整块区域,从导航栏的边缘向内收进一些,而不是紧贴图标和标签的胶囊形。选择改变时,它保持完整的大小滑到新的目的地;沿导航栏拖动时,它跟着指针移动。悬停、按下和聚焦也画在同一个框里。variant 在 tonal(默认)和 filled 之间选择。其他属性和事件在两个版本中都相同。上方标记为“待跟进”的平台在跟上之前提供的仍是第一次修订的版本:没有 variant,不能拖动,指示器只在图标背后,并且从中心向外展开;这些平台的示例展示的也是那个版本。本页的演示是目前提供的 Web 组件。

四个目的地,其中一个带有计数

filled 变体:按住并沿导航栏拖动,指示器会跟着移动

由链接组成的导航栏:页面用自己的地址指明当前目的地,“帮助”在新标签页中打开

用法

导航栏只有一种项,即 destination(目的地):一个图标,图标下方的标签,以及图标上可选的徽标。每个平台都按自己接收重复子项的方式来接收目的地:在 Web 上是 <tanrei-navigation-bar-item> 子元素,在 SwiftUI 上是视图构建器中的 TanreiNavigationBarItem 视图,在 Compose 上是内容 lambda 中的 TanreiNavigationBarItem 调用,在 Flutter 上是 destinations: 列表中的 TanreiNavigationDestination 对象,在 React Native 上是由普通对象组成的 destinations 数组。

选择属于导航栏,以目的地的值来表示。value 指明当前目的地,change 带着用户选中的那个目的地触发;用户再次选择当前目的地时触发 reselect,你可以借此把那个屏幕滚回顶端。空值表示不选中任何一个,用于不属于导航栏任何目的地的屏幕。SwiftUI 把新值写入 selection 绑定。有两个平台保留了自己的惯用方式,与它们自带的导航栏一致。Compose 没有导航栏级别的值:由你把当前项标记为 selected,它的 onClick 同时充当 change 和 reselect。Flutter 按索引选择:selectedIndex 和 onDestinationSelected,选择当前目的地时它同样会调用后者。

variant 决定当前目的地标示得有多强:tonal(默认)以柔和的颜色填充指示器,filled 以强烈的颜色填充,其上的图标和标签反色显示。目前只有 Web 提供它。

带 href 的目的地是链接:它是平台自己的链接,画法与按钮目的地完全相同。Enter 激活它,Space 不会。按下时导航栏不会选中链接:它带着目的地的值和它的 href 触发 change,随后浏览器才跟随链接。由到达的页面通过 value 指明当前目的地,与导航抽屉一样;没有 value 的链接以它的 href 作为值。target 和 targetLabel 与 Button 的相同。目前只有 Web 提供链接目的地;SwiftUI、Compose、Flutter 和 React Native 还没有。

目的地的 icon 是设计系统图标字体中一个图标的名称。未选中时导航栏以轮廓样式绘制它,选中时以填充样式绘制。badge、badgeCount 和 badgeLabel 在图标上绘制一个 Tanrei 徽标:没有计数时是圆点,否则是数字。SwiftUI、Compose 和 Flutter 以一个 TanreiNavigationBadge 值接收它们,React Native 则以一个 badge 对象接收。

导航栏是一块布局区域,不是浮层。它的颜色会延伸到它下方的主屏幕指示条或手势条之下,它的目的地则位于这些区域的上方,并在两侧的边衬区(insets)之内。在 React Native 上,边衬区只来自属性,不依赖任何安全区域库:TanreiProvider 的 insets,或导航栏自己的 insets,后者指定的边会替换 provider 的对应值。

<!-- 宿主元素是一个导航地标,由 label 命名。 -->
<!-- variant="filled" 让当前目的地反色显示,适合位于内容繁杂的屏幕之上的导航栏。默认是 tonal。 -->
<tanrei-navigation-bar label="主导航" value="inbox">
  <tanrei-navigation-bar-item value="inbox" label="收件箱" icon="inbox"
                              badge badge-count="3" badge-label="3 封未读"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="chat" label="聊天" icon="chat"
                              badge badge-label="有新消息"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="starred" label="星标" icon="star"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="settings" label="设置" icon="settings" disabled></tanrei-navigation-bar-item>
</tanrei-navigation-bar>

<script type="module">
  import '@tanrei/web/components/navigation-bar';
  const bar = document.querySelector('tanrei-navigation-bar');
  // 导航栏先设置自己的 value,再触发携带该值的 change。
  bar.addEventListener('change', (e) => showScreen(e.detail.value));
  bar.addEventListener('reselect', () => scrollTo({ top: 0 }));
</script>

<!-- 由链接组成的导航栏:每个页面把 value 设为自己的地址。没有 value 的链接以它的 href 作为值。 -->
<tanrei-navigation-bar label="主导航" value="/search/">
  <tanrei-navigation-bar-item href="/" label="首页" icon="home"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="/search/" label="搜索" icon="search"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="/library/" label="媒体库" icon="video_library"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="https://example.com/help" label="帮助" icon="help"
                              target="new" target-label="在新标签页中打开"></tanrei-navigation-bar-item>
</tanrei-navigation-bar>

<script type="module">
  // 由路由接管导航:取消 change,前往 detail.href,并说明产品现在位于哪里。
  bar.addEventListener('change', (e) => {
    if (!e.detail.href) return;
    e.preventDefault();
    router.push(e.detail.href);
    bar.value = e.detail.value;
  });
</script>
import { NavigationBar, NavigationBarItem } from '@tanrei/react';

// variant="filled" 适合位于内容繁杂的屏幕之上的导航栏;默认是 tonal。
<NavigationBar label="主导航" value={screen} onChange={(e) => setScreen(e.detail.value)} onReselect={scrollToTop}>
  <NavigationBarItem value="inbox" label="收件箱" icon="inbox" badge badgeCount={3} badgeLabel="3 封未读" />
  <NavigationBarItem value="chat" label="聊天" icon="chat" />
  <NavigationBarItem value="starred" label="星标" icon="star" />
</NavigationBar>
import Tanrei

// 放在底部安全区域 inset 中的独立导航栏。选择目的地时会写入绑定(change)。
ScreenView(screen)
    .safeAreaInset(edge: .bottom, spacing: 0) {
        TanreiNavigationBar(selection: $screen, accessibilityLabel: "主导航", onReselect: { _ in scrollToTop() }) {
            TanreiNavigationBarItem("收件箱", icon: "inbox", value: "inbox",
                                    badge: TanreiNavigationBadge(count: 3, label: "3 封未读"))
            TanreiNavigationBarItem("聊天", icon: "chat", value: "chat")
            TanreiNavigationBarItem("星标", icon: "star", value: "starred")
            // disabled 是 SwiftUI 自己的修饰符。
            TanreiNavigationBarItem("设置", icon: "settings", value: "settings").disabled(true)
        }
    }
import io.github.chnotchy.tanrei.components.TanreiNavigationBadge
import io.github.chnotchy.tanrei.components.TanreiNavigationBar
import io.github.chnotchy.tanrei.components.TanreiNavigationBarItem

// 与 Material 的 NavigationBar 一样:没有导航栏级别的值。由你把当前项标记为 selected,
// 同一个 onClick 既是 change 也是 reselect。
var screen by rememberSaveable { mutableStateOf("inbox") }
TanreiNavigationBar(contentDescription = "主导航") {
    TanreiNavigationBarItem(
        selected = screen == "inbox",
        onClick = { if (screen == "inbox") { scrollToTop() } else { screen = "inbox" } },
        icon = "inbox",
        label = "收件箱",
        badge = TanreiNavigationBadge(count = 3, contentDescription = "3 封未读"),
    )
    TanreiNavigationBarItem(selected = screen == "chat", onClick = { screen = "chat" }, icon = "chat", label = "聊天")
    // enabled 与规格中的 disabled 相反。
    TanreiNavigationBarItem(selected = screen == "settings", onClick = { screen = "settings" }, icon = "settings", label = "设置", enabled = false)
}
import 'package:tanrei/tanrei.dart';

// 与 NavigationBar 一样:目的地的值就是它的索引。导航栏自己不保存选择。
TanreiNavigationBar(
  selectedIndex: current, // null 表示不选中任何一个
  onDestinationSelected: (int index) {
    if (index == current) {
      scrollToTop(); // reselect
    } else {
      setState(() => current = index); // change
    }
  },
  semanticLabel: '主导航',
  destinations: const <TanreiNavigationDestination>[
    TanreiNavigationDestination(icon: 'inbox', label: '收件箱', badge: TanreiNavigationBadge(count: 3, semanticLabel: '3 封未读')),
    TanreiNavigationDestination(icon: 'chat', label: '聊天'),
    TanreiNavigationDestination(icon: 'settings', label: '设置', enabled: false),
  ],
),
import { useState } from 'react';
import { NavigationBar } from '@tanrei/react-native';

const [screen, setScreen] = useState('inbox');

// 受控:在 onValueChange 中更新 value。
<NavigationBar
  accessibilityLabel="主导航"
  value={screen}
  onValueChange={setScreen}
  onReselect={scrollToTop}
  destinations={[
    { value: 'inbox', label: '收件箱', icon: 'inbox', badge: { count: 3, label: '3 封未读' } },
    { value: 'chat', label: '聊天', icon: 'chat', badge: { label: '有新消息' } },
    { value: 'settings', label: '设置', icon: 'settings', disabled: true },
  ]}
/>

规格

只显示选中项的标签,以及不显示标签

构成

部件 说明
1 container The bar along the bottom edge, and the ground the active indicator moves on: one constant color, whichever destination is selected. Its color reaches under the system inset below it (home indicator, gesture bar).
2 outline The bar's top edge, which sets it apart from the content above.
3 item One destination (an item of the destination kind): an equal share of the bar's width, less the bar's inset at its two ends, and its full height. The whole cell is the hit area.
4 item.container The destination's box: its cell, held in from the bar's top and bottom edges by the bar's inset. It is as wide as the destination's share of the bar, not as wide as its icon and label, so every destination of a bar has the same box. It carries hover, focus and press for every destination, which are therefore drawn where the active indicator is or would be, and it is filled as the active indicator while the destination is selected. When the selection changes the filled box slides from the previous destination to the new one, and it follows a finger dragged across the bar.
5 item.icon-indicator The box behind the icon alone, the icon centered in it. The default system leaves it transparent; a system that marks the selection behind the glyph only (Material's navigation bar) fills it and leaves the item container clear. It grows in from its center rather than sliding.
6 item.icon The destination's glyph (its icon). Outlined when unselected, filled when selected.
7 item.label · 可选 The destination's name under the icon indicator, inside the item container, on one line. Always its accessible name, drawn or not.
8 item.covered-content A second, decorative copy of the icon and label in the covered colors, clipped to the active indicator wherever it is drawn, so that the content inverts where the indicator covers it and the inversion travels with the indicator, in a slide and under a finger alike. Hidden from assistive technology.
9 item.badge · 可选 A Tanrei badge anchored to the icon, drawn from the destination's badge, badgeCount and badgeLabel: a dot or a count (the badge component). It is drawn above both copies of the icon.
10 focus-ring Gap-then-ring indicator around the item container, concentric with it and in its shape, held inside the bar, shown for keyboard focus.

属性

属性 类型 默认值 说明
value string "" The value of the selected destination (selection is the bar's, by a destination's value). Empty selects none (the current screen is not one of the bar's destinations).
variant tonal | filled "tonal" How the active indicator is filled. Tonal: a soft container with the content in that container's own color. Filled: the strong color, with the content inverted on it, for a bar that must stand out from a busy screen. Tonal destinations are drawn from the unselected and selected conditions, filled ones from filled-unselected and filled-selected. The indicator's size and place are the same in both.
labelVisibility always | selected | never "always" Which destinations draw their label: all of them, only the selected one, or none. A label that is not drawn is still the destination's accessible name, and no item container changes size with it.
label string "" Names the bar itself for assistive technology ("Main"); on the web it names the navigation landmark.

事件

事件 说明
change The user activated a destination that is not the selected one, or released a drag of the active indicator on one; carries its value and, for a link, its href. For a link it comes before the link is followed, only for an activation that follows it in place (NVB-31), and a product that cancels it follows the link itself.
reselect The user activated the destination that is already selected (a product may scroll that screen to its top); carries its value and, for a link, its href, as change does. A drag that returns the indicator to it does not emit it.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

destination

One top-level destination of the bar: an icon, a label under the icon, and optionally a badge on the icon.

属性 类型 默认值 说明
value string "" What identifies the destination. The bar's value selects the destination whose value equals it; change and reselect carry it. Unique within the bar. Empty on a link, its href stands as its value.
href string "" The address the destination leads to. Not empty, the destination is a link and not a button (NVB-30): it is followed as the platform follows a link, and the bar does not select it by itself (NVB-31). Empty, the destination is a button and the product moves on change. As the Button's href (a control with a destination is a link).
target same | new "same" Where a link opens, as the Button's target. same: where the person is. new: a new context — a new tab or window on the web, outside the current screen elsewhere — which a bar's cell has no place to mark and says through targetLabel (NVB-32). A link that opens elsewhere is never selected by a press (NVB-31). Ignored without href.
targetLabel string "" What assistive technology reads after the link's name while target is new ("opens in a new tab"), as the Button's targetLabel: Tanrei ships no words of its own. Required while target is new; ignored otherwise.
label string "" Its name, drawn under the icon (see the bar's labelVisibility) and always its accessible name. Required.
icon string "" The name of its glyph in the system's icon set, drawn in the destination's own icon style: outlined while unselected, filled while selected. Required.
disabled boolean false Makes it unavailable. It keeps its place, draws its disabled look, cannot be activated and leaves the focus order.
badge boolean false Draws a Tanrei badge (the badge component) on the icon, from badgeCount and badgeLabel.
badgeCount number null The badge's count, as the badge component's count: null draws the dot, 0 draws nothing, above 0 the number (above 999 as 999+). Ignored while badge is false.
badgeLabel string "" The badge's label, as the badge component's: what assistive technology reads after the destination's name and state ("3 unread"). A dot needs one. Ignored while badge is false.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-navigation-bar-item TanreiNavigationBarItem TanreiNavigationBarItem TanreiNavigationDestination NavigationBarDestination
value · 属性 value value selected destinations value
href · 属性 href destination href href href
target · 属性 target target target target target
targetLabel · 属性 targetLabel targetLabel targetLabel targetSemanticLabel targetLabel
label · 属性 label title label label label
icon · 属性 icon icon icon icon icon
disabled · 属性 disabled disabled enabled enabled disabled
badge · 属性 badge badge badge badge badge
badgeCount · 属性 badgeCount count count count count
badgeLabel · 属性 badgeLabel label contentDescription semanticLabel label
备注 icon is the glyph's name, as <tanrei-tab>'s. badgeCount and badgeLabel are the badge-count and badge-label attributes; the element draws a <tanrei-badge> around the glyph from them. href is the spec's own prop: the cell is an <a href> in the item's shadow root. target is the target attribute with the values same and new (the HTML spellings _self and _blank are read as those two), targetLabel the target-label attribute, and rel (to which new adds noopener) and download are web-only and forwarded to the anchor, all as <tanrei-button>'s. Space is not handled on it. With no value attribute the item's value property reads its href attribute as written (not the resolved URL). TanreiNavigationBarItem(_ title: String, icon: String, value: Value, badge: TanreiNavigationBadge? = nil), where icon is the glyph's name and TanreiNavigationBadge(count: Int? = nil, label: String = "") is drawn with .tanreiBadge(…) on the glyph (no badge is nil). disabled follows .disabled(_:) on the item. destination: URL? = nil makes it a link (the link trait beside the item's selected trait), opened by the environment's openURL after the selection callback, which an app that routes addresses itself answers with its own OpenURLAction; the binding is not written for it. target: TanreiLinkTarget = .same and targetLabel (the accessibility hint), as the Button's. TanreiNavigationBarItem(selected, onClick, icon = "inbox", label = "…", enabled = true, badge = null) in the bar's RowScope content lambda. enabled is the inverse flag. badge: TanreiNavigationBadge? with TanreiNavigationBadge(count: Int? = null, contentDescription: String = ""); the item draws a TanreiBadge around its icon from it. icon is the glyph's name; the item draws it in the selection's icon style. href: String? = null makes it a link: opened through LocalUriHandler.current after onClick (an app that routes addresses itself provides its own UriHandler); the item keeps Role.Tab, which Compose cannot join with a link role. target: TanreiLinkTarget = TanreiLinkTarget.Same, and targetLabel is appended to the state description, as the Button's. TanreiNavigationDestination(icon:, label:, enabled: true, badge: null) in the bar's destinations: list; its value is its index there. icon is the glyph's name. badge: TanreiNavigationBadge(count:, semanticLabel:), drawn as a TanreiBadge around the icon. enabled: false disables it. href: Uri? = null makes it a link (Semantics(link: true) with the address as its linkUrl, beside the tab semantics), opened by the nearest TanreiLinkOpener after onDestinationSelected; target is a TanreiLinkTarget and targetSemanticLabel the semantics hint, as the Button's. An entry of destinations: NavigationBarDestination[]: { value, label, icon, disabled?, badge? }, where icon is the glyph's name and badge?: { count?: number | null; label?: string } (absent is no badge) is drawn as a Badge around the icon. href?: string makes it a link: the destination keeps the tab role (React Native gives an element one role), onValueChange receives the value and the href, and Linking.openURL(href) follows it unless the product handles it; target is "same" | "new" and targetLabel the accessibilityHint, as the Button's.

状态

行为

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

ID 需求
NVB-1 WHEN the user activates a destination by pointer, touch, Enter or Space (Enter only for a destination that is a link, and for a link only an activation that NVB-31 counts) THE navigation bar SHALL emit change exactly once with the destination's value WHILE it is not the selected destination, and reselect exactly once with its value WHILE it is.
NVB-2 WHILE a destination's disabled is true THE navigation bar SHALL NOT emit change or reselect for it, SHALL NOT make it focusable and SHALL expose it as disabled to assistive technology.
NVB-3 THE navigation bar SHALL draw the destination whose value equals the bar's value from its variant's selected condition and every other destination from its variant's unselected condition: WHILE variant is tonal the selected condition is selected (comp.navigation-bar.selected.item.container.color, comp.navigation-bar.selected.item.icon-indicator.color, comp.navigation-bar.selected.item.icon.color, comp.navigation-bar.selected.item.label.color, comp.navigation-bar.selected.item.covered-icon.color, comp.navigation-bar.selected.item.covered-label.color) and the unselected condition is unselected; WHILE variant is filled they are filled-selected (comp.navigation-bar.filled-selected.item.container.color and the same parts) and filled-unselected. IF no destination's value equals a value that is not empty THE navigation bar SHALL draw every destination from its variant's unselected condition, and in debug builds report the value.
NVB-4 WHILE a destination is hovered, keyboard-focused or pressed THE navigation bar SHALL render its item container, icon indicator, icon, label, covered icon and label, and opacity from the component tokens of its condition in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state.
NVB-5 WHEN a destination receives keyboard focus THE navigation bar SHALL show the focus indicator (the ring or the glow, NVB-10) around its item container (NVB-12), concentric with it and in its shape, with its inner edge sys.focus.offset outside the container's edge, and SHALL hold it inside the bar: WHERE the space between the item container and the bar's edges (comp.navigation-bar.container.padding) is less than the indicator reaches, it SHALL be drawn further in by the difference, on every side alike, keeping the container's shape. THE indicator SHALL be drawn above the neighbouring destinations and the active indicator, without animation; focus gained by pointer or touch SHALL NOT show it.
NVB-6 THE navigation bar SHALL make each destination's hit area its whole cell, the bar's full height above the system inset and its share of the bar's width (NVB-11), the first and the last cell reaching the bar's start and end edges, and SHALL NOT let a cell's hit area fall below sys.target.touch in either direction; hover and press over any part of the cell SHALL answer on the item container. WHERE an equal share of the bar's width is narrower than sys.target.touch (more destinations than the width holds), the cells SHALL still share the bar's width and narrow below the target rather than make the bar wider than the space it is given.
NVB-7 WHILE a destination's disabled is true THE navigation bar SHALL render its item container, icon indicator, icon, label, covered icon and label, and opacity from the disabled component tokens of its condition.
NVB-8 WHEN a destination is pressed by pointer, touch, Enter or Space THE navigation bar SHALL show its pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
NVB-9 WHILE a destination is pressed THE navigation bar SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole item container at once; ripple grows the pressed state layer, clipped to the item container, from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole destination and draws no state layer; none draws no answer to the press. Under ripple and dim only the item container color stays with the state under the press; icon indicator, icon, label, covered icon and label, and opacity still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.navigation-bar.motion.feedback-duration with comp.navigation-bar.motion.state-easing, fades over comp.navigation-bar.motion.state-duration with comp.navigation-bar.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
NVB-10 THE navigation bar SHALL ease its item container and icon indicator corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
NVB-11 THE navigation bar SHALL span the full width of its parent, at least comp.navigation-bar.container.height high above the system inset, filled with comp.navigation-bar.container.color, raised by comp.navigation-bar.container.elevation and edged at the top by comp.navigation-bar.outline.width of comp.navigation-bar.outline.color, and SHALL divide its width, less comp.navigation-bar.container.padding at its start and at its end, into equal cells, one per destination, in reading order from the start edge; right-to-left layouts SHALL mirror the order. THE bar's container SHALL be the same whichever destination is selected, whatever state a destination is in and while the active indicator moves: only the active indicator marks the selection.
NVB-12 THE navigation bar SHALL draw each destination's item container as wide as its cell and as tall as the bar above the system inset less comp.navigation-bar.container.padding above and below, the same size for every destination of the bar whatever each holds, with its four corners rounded by comp.navigation-bar.item.container.shape, each at most half its height, and SHALL NOT size it to the destination's content. Inside it THE navigation bar SHALL lay out the destination as a column centered in the item container: the icon indicator, comp.navigation-bar.item.icon-indicator.width wide and comp.navigation-bar.item.icon-indicator.height high with corners comp.navigation-bar.item.icon-indicator.shape, its icon centered in it; and WHILE the label is drawn, the label below it, comp.navigation-bar.item.gap away, centered, on one line in comp.navigation-bar.item.label.typescale. THE column SHALL keep comp.navigation-bar.item.container.padding-block between itself and the item container's top and bottom edges, the bar growing in height to keep it (NVB-21), and a label wider than the item container less comp.navigation-bar.item.container.padding-inline on each side SHALL be truncated at its end with an ellipsis. THE navigation bar SHALL NOT draw hover, focus or press on the part of the cell outside the item container. Every part is laid out in every system; a transparent color hides a part without changing the layout.
NVB-13 WHILE labelVisibility is always THE navigation bar SHALL draw every destination's label; WHILE it is selected it SHALL draw only the selected destination's label; WHILE it is never it SHALL draw none. A destination that draws no label SHALL center its icon indicator in its item container, whose size does not change with the label, and every destination's label SHALL remain its accessible name.
NVB-14 THE navigation bar SHALL draw each icon at comp.navigation-bar.item.icon.size with the family and fill of its condition (selected for the selected destination, unselected for every other), sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max.
NVB-15 WHEN the selected destination changes while no drag is followed (NVB-27) THE navigation bar SHALL slide the active indicator (the fill of the selected condition's item container) from the previous destination's item container to the new one's, at its full size throughout and drawn in the new destination's colors, every frame of the slide being the in-between look of NVB-27 (the indicator as a drag would draw it at that place, each icon and label in its covered color exactly where it is covered); SHALL grow the new destination's icon indicator in width from its center to its full width, at its full height throughout, and fade the previous one out; and SHALL change icon and label colors and icon fill; all over comp.navigation-bar.motion.selection-duration with comp.navigation-bar.motion.selection-easing. WHERE no destination was selected before, the active indicator SHALL fade in on the new item container at its full size, and WHERE none is selected after, it SHALL fade out, over the same time; it SHALL NOT grow or shrink in width to appear or disappear. WHILE labelVisibility is selected the labels SHALL appear and disappear over the same time, without resizing or moving any item container. WHERE motion is reduced it SHALL show the new selection at once. WHEN the bar first appears, or the selected destination changes size or place without a new selection (a font arriving, the text size changing, the bar resizing), THE navigation bar SHALL put the active indicator on it at once, without a slide.
NVB-16 WHILE a destination's badge is true THE navigation bar SHALL draw a Tanrei badge anchored to its icon as the badge component draws one whose count is the destination's badgeCount and whose label is its badgeLabel, SHALL keep it while the destination is disabled, and SHALL expose the badge's description (badgeLabel, or the displayed count when it is empty) as part of the destination's own description, after its name and state.
NVB-17 IF the navigation bar holds fewer than three or more than five destinations THE navigation bar SHALL still render them, and in debug builds report the count.
NVB-18 THE navigation bar SHALL extend its container under the bottom system inset (home indicator, gesture bar, taskbar) and SHALL place its destinations above that inset and inside the left and right insets.
NVB-19 THE navigation bar SHALL expose each destination as one element named by its label, with its selected state and its position in the set (for example 2 of 4), and its icon hidden: a link WHILE its href is not empty (NVB-30) and a button otherwise. On the web it is an anchor or a button inside a navigation landmark named by the bar's label, the selected destination carrying aria-current="page"; on Apple platforms, Android, Flutter and React Native a tab of a tab bar, with the selected state, a link destination included: there the tab is what carries the position and the selection a bar is read by, and a link destination adds the link trait to it where the platform lets one element carry both. The selected destination is the place the product is showing, in a product of one screen and in a site of many pages alike: THE navigation bar SHALL have no second state and no second mark for a current page, and SHALL expose exactly one destination, or none, as current.
NVB-20 WHILE a destination's label is not empty THE navigation bar SHALL expose it as the destination's accessible name, over a name the product gives the destination on the element (aria-label or aria-labelledby); WHILE a destination's label is empty and the product gives the destination a name of its own on the element — aria-label, or aria-labelledby naming an element — THE navigation bar SHALL expose that name as its accessible name; WHILE a destination's label is empty and the product gives it none THE navigation bar SHALL still render it, and in debug builds report that it has no accessible name.
NVB-21 WHILE the text size is enlarged THE navigation bar SHALL grow its height to hold its labels on one line inside their item containers (NVB-12), every item container growing with the bar alike, and SHALL NOT scale its icon indicators or icons; WHERE the platform offers a large-content viewer for bar items (iOS) a long press SHALL show the destination's icon and label enlarged.
NVB-22 IF two destinations have the same value THE navigation bar SHALL treat the first of them as the one the bar's value selects, and in debug builds report the repeated value.
NVB-23 THE navigation bar SHALL draw each destination's icon and label a second time, over the first, in the covered colors of its condition (comp.navigation-bar.unselected.item.covered-icon.color and comp.navigation-bar.unselected.item.covered-label.color for a tonal destination that is not selected, comp.navigation-bar.selected.item.covered-icon.color and comp.navigation-bar.selected.item.covered-label.color for the tonal selected one, comp.navigation-bar.filled-unselected.item.covered-icon.color, comp.navigation-bar.filled-unselected.item.covered-label.color, comp.navigation-bar.filled-selected.item.covered-icon.color and comp.navigation-bar.filled-selected.item.covered-label.color for a filled bar), clipped to the active indicator's current shape, its corners included, wherever the indicator is drawn; WHILE the indicator slides (NVB-15) or follows a drag (NVB-27) the clip SHALL move with it in the same frame, so that a destination it passes over changes where it is covered and the newly selected destination's icon and label are wholly in their covered colors when it lands, neither before nor after. THE navigation bar SHALL NOT approximate the covered colors with a blend mode, SHALL draw the badge above both copies, and SHALL expose each destination to assistive technology once.
NVB-24 THE navigation bar 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 navigation bar is shown THE navigation bar SHALL draw itself for the new direction at once, without a motion of its own, keeping the selected destination and its focus; a drag (NVB-27) SHALL carry the active indicator to where the pointer is in either direction.
NVB-25 WHERE the layout direction is right-to-left THE navigation bar SHALL draw a destination's icon, in both of its copies (NVB-23) mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order NVB-11 gives. THE navigation bar SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name.
NVB-26 THE navigation bar SHALL lay out a destination's label, in both of its copies (NVB-23) as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE navigation bar SHALL still place each label and align its lines by the layout direction, not by the text's own, and SHALL cut a label that does not fit (NVB-12) at the end of its own direction, so that a left-to-right label in a right-to-left layout loses its right end and never its beginning.
NVB-27 WHEN the user presses the bar with touch, a pen or a mouse and drags past the platform's touch slop (8 points where the platform names none), further sideways than up or down, WHILE a destination is selected, THE navigation bar SHALL take the drag: the press SHALL activate no destination when it ends (NVB-1), the destination under it SHALL leave its pressed state as it does when a press ends (NVB-8), and from then until the drag ends THE navigation bar SHALL draw the active indicator where the pointer is along the bar — on an enabled destination's item container while the pointer is at that cell's center, halfway between two neighbouring enabled destinations while the pointer is where their cells meet (the middle of any disabled destinations between them), at the linear interpolation in between, and on the first or last enabled destination while the pointer is before or beyond its center — at its full size, in the selected destination's colors. Everything that differs between the indicator on the one destination and on the other SHALL change continuously with the pointer, with no threshold: its place, and the color of each icon and label, which is its covered color exactly where the indicator covers it and its own color elsewhere, the clip of NVB-23 moving with the indicator in the same frame. The icon indicator, each icon's fill and, WHILE labelVisibility is selected, the label that is drawn SHALL stay with the selected destination until the release (NVB-28). THE navigation bar SHALL place the indicator in the frame the pointer moves, without a motion of its own; WHEN the drag is taken with the pointer away from where the indicator rests THE navigation bar SHALL bring it to the pointer's place over comp.navigation-bar.motion.state-duration with comp.navigation-bar.motion.state-easing instead of at once, following the pointer meanwhile. A drag that is further up or down than sideways when it passes the slop SHALL be left to the page and to the system's gestures at the bottom edge for its whole length, a wheel or a trackpad SHALL move nothing, and WHILE no destination is selected THE navigation bar SHALL take no drag.
NVB-28 WHEN the user releases a drag the navigation bar has taken THE navigation bar SHALL choose one enabled destination: the one the active indicator is nearer to of the two it lies between (the one after it from halfway on), or, WHEN the release is a fling along the bar, the one of those two that the fling is toward; a fling is a release faster than the platform's minimum fling velocity (Android's ViewConfiguration.getScaledMinimumFlingVelocity, Flutter's kMinFlingVelocity) or, where the platform names none (the web, SwiftUI, React Native), 0.5 points per millisecond. A release's velocity is the pointer's over its last moves: WHERE the pointer has not moved for longer than the time after which the platform's velocity tracker takes a pointer as stopped (40 milliseconds in Android's VelocityTracker, Jetpack Compose's VelocityTracker and Flutter's VelocityTracker) or, where the platform names none (the web, SwiftUI, React Native), 40 milliseconds before the release THE navigation bar SHALL take the release as having no velocity, so that a drag that stops where it is wanted and is let go there is no fling, however fast it moved before it stopped. WHERE the chosen destination is not the selected one and is not a link THE navigation bar SHALL activate it as NVB-1 does — emit change exactly once with its value — and SHALL slide the active indicator from where the release left it onto that destination over comp.navigation-bar.motion.selection-duration with comp.navigation-bar.motion.selection-easing, as NVB-15 does from a destination; WHERE the chosen destination is not the selected one and is a link THE navigation bar SHALL treat the release as a plain press on that link — emitting change and following the link as NVB-31 says, or only opening it where NVB-31 does not count it — SHALL NOT write value, and SHALL return the active indicator to the destination that value selects as NVB-29 returns it, the indicator going on to the chosen destination when the product sets value (NVB-15); WHERE it is the selected destination THE navigation bar SHALL return the indicator to it over the same motion and SHALL emit neither change nor reselect. The place where two cells meet decides only what a release chooses; nothing drawn changes at it. The release SHALL NOT also activate the destination under the pointer.
NVB-29 WHEN a drag the navigation bar has taken is cancelled — the platform cancels the pointer (a system gesture, a lost capture), Escape is pressed while it lasts, or the product sets value or removes or disables a destination the indicator lies between — THE navigation bar SHALL return the active indicator to the selected destination (the one the product has just set, where it set one) over comp.navigation-bar.motion.selection-duration with comp.navigation-bar.motion.selection-easing and emit nothing. WHILE a drag lasts THE navigation bar SHALL NOT change value, keyboard focus, or the selected state that each destination exposes to assistive technology, whatever the indicator covers, and SHALL announce nothing: the selection is made, and announced, by the release alone (NVB-28). WHERE motion is reduced THE navigation bar SHALL still draw the active indicator where the pointer is, which the person's own finger moves, taking the drag without the catch-up motion of NVB-27, and SHALL put it on its destination at once when the drag is released or cancelled.
NVB-30 WHILE a destination's href is not empty THE navigation bar SHALL make it a link to that address and not a button. On the web it SHALL be an anchor element with that address, whose behaviours are the browser's own — following it on a plain click, a touch or Enter; opening it in another tab or window by a modified click or a middle click; the link's context menu; copying its address; showing its address on hover — and THE navigation bar SHALL NOT prevent, replace or imitate any of them, with one exception: a sideways drag that begins on a link destination moves the active indicator (NVB-27) and does not drag the link's address, because one gesture cannot do both and the drag is the bar's own way of moving its selection; Space SHALL NOT activate a link. On other platforms it SHALL be the platform's own link (SwiftUI's Link; on Compose, Flutter and React Native an element with the platform's link semantics whose address is opened by the platform's opener), as BTN-22 says for a link button. THE navigation bar SHALL draw a link exactly as it draws a button destination, from the same tokens in every state and both variants (NVB-3, NVB-4, NVB-7 to NVB-9, NVB-12, NVB-23), SHALL draw no look of its own for a link the user has visited, and SHALL give its hit area (NVB-6) to the link. WHILE a link's value is empty THE navigation bar SHALL take its href as its value, for value, change, reselect and NVB-22. WHILE a link's disabled is true it SHALL keep the link role with the disabled state and no destination, and SHALL open nothing by any means the platform gives a link (NVB-2; on the web the anchor carries no address).
NVB-31 WHEN the user activates a link destination THE navigation bar SHALL count it as an activation (NVB-1) only WHILE it follows the link in the place where the product is shown: a click with the primary button and no modifier key, a touch, Enter, or the default action of assistive technology, on a link whose target is same and that asks for no download. It SHALL then emit change or reselect exactly once, carrying the destination's value and its href, before the link is followed, and SHALL NOT hold the link back for sys.motion.duration.press-hold; SHALL NOT write value itself on any platform, because where the product is, is the product's to say, and the page or the route shown next sets value; SHALL keep the active indicator on the destination that value selects, moving it as NVB-15 says only when value changes; and SHALL let the link be followed unless the product cancels the event (preventDefault on the web), as a product does that moves between its pages without loading a document. For every other activation of a link — a modified or a middle click, a link whose target is new, a download — THE navigation bar SHALL emit neither change nor reselect, and SHALL keep value and the drawn selection as they are.
NVB-32 WHILE a destination is a link and its target is new THE navigation bar SHALL open its href in a new context, as BTN-24 says for a link button (on the web a new tab or window that is given no handle to the page that opened it), and SHALL draw no mark for it: a destination's cell is an icon over a label and has no trailing glyph place for a mark to stand in, as an icon-only button has none, and a second glyph beside the icon would be read as its badge. It SHALL expose targetLabel as part of the destination's description, after its badge's description (NVB-16), and WHILE targetLabel is empty SHALL still render and in debug builds report that a link that opens a new context does not say so. WHILE target is same THE navigation bar SHALL ignore targetLabel.
NVB-33 WHEN a destination's target names a value the navigation bar does not define THE navigation bar SHALL treat it as same, and in debug builds report the unknown value. IF a destination's href is only white space, or names a script to run rather than a place to go (the web's javascript: scheme), or cannot be read as an address by the platform, THE navigation bar SHALL make the destination a button without a destination, and in the last two cases report it in debug builds, as BTN-26 says for a button. WHILE a destination's href is empty THE navigation bar SHALL ignore its target and targetLabel.
NVB-34 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on a destination's host, by the attribute or by the ARIA property that reflects it, THE navigation bar SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries the destination's role (its link or its button) — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the destination is invalid — except those a destination owns (aria-current, aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE navigation bar SHALL take it off that element. WHEN the product sets an aria- attribute a destination owns THE navigation bar SHALL report it in debug builds. The bar's host carries its navigation landmark itself (NVB-19), so what the product writes on it is not moved, but for a name: WHILE the bar's label is not empty THE navigation bar SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the landmark's whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written, so that a name the product gives names the landmark; WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE navigation bar SHALL take it away.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-navigation-bar TanreiNavigationBar TanreiNavigationBar TanreiNavigationBar NavigationBar
value · 属性 value selection selected selectedIndex value
variant · 属性 variant variant variant variant variant
labelVisibility · 属性 labelVisibility labelVisibility labelVisibility labelBehavior labelVisibility
label · 属性 label accessibilityLabel contentDescription semanticLabel accessibilityLabel
change · 事件 change selection onClick onDestinationSelected onValueChange
reselect · 事件 reselect onReselect onClick onDestinationSelected onReselect
备注 Destinations are <tanrei-navigation-bar-item> children, in order. The bar selects the activated destination itself (sets value) before firing change, as a native control does, unless the destination is a link. For a link destination change and reselect are cancelable, carry detail.href beside detail.value, and are fired from the anchor's click only when event.button is 0, no modifier key is down, the click was not already cancelled, and the item's target is same and it has no download; cancelling one calls preventDefault() on that click. The bar does not set value for a link. variant is the variant attribute (tonal, filled) and labelVisibility the label-visibility attribute. The host is a navigation landmark (role="navigation", aria-label from label); the selected destination has aria-current="page". The active indicator is one layer of the bar that slides between the destinations' item containers; the covered copy of every icon and label (NVB-23) is an aria-hidden layer clipped with clip-path to the indicator's box, animated with it. A drag (NVB-27 to NVB-29) is followed with Pointer Events, as tanrei-tabs follows its own: pointerdown on the bar, setPointerCapture once the drag is taken, pointermove placing the indicator through the same custom properties a slide transitions, pointerup choosing the destination, and pointercancel, lostpointercapture or Escape cancelling; the click that follows a taken drag is swallowed, aria-current is written only on the release, a destination that is a link is not draggable as a link (draggable="false", the one behaviour of a link the bar takes, NVB-30) and is followed with its own click() when a release chooses it, the indicator then returning to the destination value selects (NVB-28), and touch-action: pan-y leaves vertical drags to the page. The bottom inset is env(safe-area-inset-bottom). It does not yet follow NVB-30 to NVB-33, nor the parts of NVB-1, NVB-19 and NVB-28 that concern links: a pressed link destination is selected like a button, change is not cancelable and carries no href, and the destination writes the link rules itself instead of using the shared link primitive (packages/web/src/primitives/link.ts). TanreiNavigationBar(selection: Binding<Value>, variant:, labelVisibility:, onReselect:) { TanreiNavigationBarItem(…) … }, placed with .safeAreaInset(edge: .bottom). A bar of its own rather than a restyled TabView, whose bar the tokens cannot reach (indicator, geometry, colors). Activation writes the binding (change); activating the selected item calls onReselect. The bar is exposed as a tab bar and each item as a button with the selected trait; each item supports the large-content viewer. It lags the spec: it has no variant, and draws the active indicator behind the icon alone (the item container at its least size), growing in from its center instead of sliding, with the label outside it. Not built on this platform yet: the item container as wide as the destination's cell and held in by container.padding, hover, focus and press drawn in that box, and the drag of the active indicator (NVB-27 to NVB-29): a DragGesture(minimumDistance:) beside each item's tap, predictedEndTranslation for the fling, the binding written in onEnded. Link destinations are not built yet either (NVB-30 to NVB-33 and the parts of NVB-1, NVB-19 and NVB-28 that concern links, with the fixtures links, links-rtl and links-disabled). Compose idiom (as NavigationBar): TanreiNavigationBar(variant, labelVisibility, contentDescription) { TanreiNavigationBarItem(…) … }. There is no bar-level value: the caller marks the selected item with selected = (current == value), so the bar's value and a destination's value both map to the item's selected, and one onClick per item covers change and reselect (the caller knows which item is selected). The bar draws the sliding active indicator behind its items, from the bounds of the selected item. Insets follow WindowInsets.navigationBars plus the horizontal display cutout, as a windowInsets parameter. Items have Role.Tab in a selectableGroup(). It lags the spec: it has no variant, and draws the active indicator behind the icon alone (the item container at its least size), growing in from its center instead of sliding, with the label outside it. Not built on this platform yet: the item container as wide as the destination's cell and held in by container.padding, hover, focus and press drawn in that box, and the drag of the active indicator (NVB-27 to NVB-29): pointerInput with detectHorizontalDragGestures on the bar and a VelocityTracker for the fling; the release calls the chosen item's onClick. Link destinations are not built yet either (NVB-30 to NVB-33 and the parts of NVB-1, NVB-19 and NVB-28 that concern links, with the fixtures links, links-rtl and links-disabled). Flutter idiom (as NavigationBar): TanreiNavigationBar(selectedIndex:, onDestinationSelected:, variant:, labelBehavior:, destinations: [TanreiNavigationDestination(…), …]). A destination's value is its index in destinations; a selectedIndex of null selects none. onDestinationSelected is called for the selected destination too (reselect); the caller compares. variant is TanreiNavigationBarVariant.tonal or .filled; labelBehavior is alwaysShow, onlyShowSelected or alwaysHide. Bottom inset from MediaQuery.padding via SafeArea. It lags the spec: it has no variant, and draws the active indicator behind the icon alone (the item container at its least size), growing in from its center instead of sliding, with the label outside it. Not built on this platform yet: the item container as wide as the destination's cell and held in by container.padding, hover, focus and press drawn in that box, and the drag of the active indicator (NVB-27 to NVB-29): a GestureDetector's horizontal drag callbacks on the bar, DragEndDetails.primaryVelocity for the fling; the release calls onDestinationSelected. Link destinations are not built yet either (NVB-30 to NVB-33 and the parts of NVB-1, NVB-19 and NVB-28 that concern links, with the fixtures links, links-rtl and links-disabled). Controlled, in React Native's value / onValueChange idiom, with a destinations data array; variant is 'tonal' or 'filled'. The bar has the tablist role and each destination the tab role with accessibilityState.selected; the covered copy is hidden with importantForAccessibility="no-hide-descendants" and accessibilityElementsHidden. Insets come from the bar's own insets prop, side by side, falling back to TanreiProvider's insets; the package takes no safe-area dependency, and a product that uses react-native-safe-area-context passes its useSafeAreaInsets() to either. It lags the spec: it has no variant, and draws the active indicator behind the icon alone (the item container at its least size), growing in from its center instead of sliding, with the label outside it. Not built on this platform yet: the item container as wide as the destination's cell and held in by container.padding, hover, focus and press drawn in that box, and the drag of the active indicator (NVB-27 to NVB-29): a PanResponder on the bar (or React Native Gesture Handler's Gesture.Pan() where the product has it), the release's vx for the fling; the release calls onValueChange. Link destinations are not built yet either (NVB-30 to NVB-33 and the parts of NVB-1, NVB-19 and NVB-28 that concern links, with the fixtures links, links-rtl and links-disabled).

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-navigation-bar-container-color sys.color.surface #ffffff
--tanrei-comp-navigation-bar-container-elevation sys.elevation.level0
--tanrei-comp-navigation-bar-container-height — 64px
--tanrei-comp-navigation-bar-container-padding sys.space.space50 4px
--tanrei-comp-navigation-bar-disabled-opacity — 1
--tanrei-comp-navigation-bar-filled-selected-disabled-item-container-color — #1b1b1b · 12%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-covered-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-selected-disabled-item-state-layer-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-selected-focused-item-container-color — #323232
--tanrei-comp-navigation-bar-filled-selected-focused-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-focused-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-focused-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-selected-focused-item-icon-color — #262626
--tanrei-comp-navigation-bar-filled-selected-focused-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-filled-selected-focused-item-state-layer-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-hovered-item-container-color — #2d2d2d
--tanrei-comp-navigation-bar-filled-selected-hovered-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-hovered-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-hovered-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-selected-hovered-item-icon-color — #262626
--tanrei-comp-navigation-bar-filled-selected-hovered-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-filled-selected-hovered-item-state-layer-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-item-container-color sys.color.primary #1b1b1b
--tanrei-comp-navigation-bar-filled-selected-item-covered-icon-color sys.color.on-primary #ffffff
--tanrei-comp-navigation-bar-filled-selected-item-covered-label-color sys.color.on-primary #ffffff
--tanrei-comp-navigation-bar-filled-selected-item-icon-indicator-color comp.navigation-bar.selected.item.icon-indicator.color #000000 · 0%
--tanrei-comp-navigation-bar-filled-selected-item-icon-color comp.navigation-bar.selected.item.icon.color #262626
--tanrei-comp-navigation-bar-filled-selected-item-label-color comp.navigation-bar.selected.item.label.color #1b1b1b
--tanrei-comp-navigation-bar-filled-selected-item-state-layer-color comp.navigation-bar.filled-selected.item.covered-icon.color #ffffff
--tanrei-comp-navigation-bar-filled-selected-pressed-item-container-color — #323232
--tanrei-comp-navigation-bar-filled-selected-pressed-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-pressed-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-selected-pressed-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-selected-pressed-item-icon-color — #262626
--tanrei-comp-navigation-bar-filled-selected-pressed-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-filled-selected-pressed-item-state-layer-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-container-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-covered-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-unselected-disabled-item-state-layer-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-filled-unselected-focused-item-container-color — #4b4b4b · 10%
--tanrei-comp-navigation-bar-filled-unselected-focused-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-focused-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-focused-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-focused-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-focused-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-focused-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-container-color — #4b4b4b · 8%
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-hovered-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-item-container-color comp.navigation-bar.unselected.item.container.color #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-item-covered-icon-color sys.color.on-primary #ffffff
--tanrei-comp-navigation-bar-filled-unselected-item-covered-label-color sys.color.on-primary #ffffff
--tanrei-comp-navigation-bar-filled-unselected-item-icon-indicator-color comp.navigation-bar.unselected.item.icon-indicator.color #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-item-icon-color comp.navigation-bar.unselected.item.icon.color #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-item-label-color comp.navigation-bar.unselected.item.label.color #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-item-state-layer-color comp.navigation-bar.filled-unselected.item.icon.color #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-container-color — #4b4b4b · 10%
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-covered-icon-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-covered-label-color — #ffffff
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-filled-unselected-pressed-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-focused-opacity — 1
--tanrei-comp-navigation-bar-hovered-opacity — 1
--tanrei-comp-navigation-bar-item-container-padding-block sys.space.space50 4px
--tanrei-comp-navigation-bar-item-container-padding-inline sys.space.space150 12px
--tanrei-comp-navigation-bar-item-container-shape sys.shape.corner.full 9999px
--tanrei-comp-navigation-bar-item-gap sys.space.space50 4px
--tanrei-comp-navigation-bar-item-icon-indicator-height comp.navigation-bar.item.icon.size 24px
--tanrei-comp-navigation-bar-item-icon-indicator-shape sys.shape.corner.full 9999px
--tanrei-comp-navigation-bar-item-icon-indicator-width comp.navigation-bar.item.icon.size 24px
--tanrei-comp-navigation-bar-item-icon-size sys.icon.medium 24px
--tanrei-comp-navigation-bar-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-navigation-bar-motion-selection-duration sys.motion.role.selection.travel 250ms
--tanrei-comp-navigation-bar-motion-selection-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-navigation-bar-motion-state-duration sys.motion.role.state.large 250ms
--tanrei-comp-navigation-bar-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-navigation-bar-outline-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-navigation-bar-outline-width sys.border.hairline 1px
--tanrei-comp-navigation-bar-pressed-opacity — 1
--tanrei-comp-navigation-bar-selected-disabled-item-container-color — #eeeeee
--tanrei-comp-navigation-bar-selected-disabled-item-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-selected-disabled-item-covered-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-selected-disabled-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-selected-disabled-item-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-selected-disabled-item-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-selected-disabled-item-state-layer-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-selected-focused-item-container-color — #dadada
--tanrei-comp-navigation-bar-selected-focused-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-selected-focused-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-selected-focused-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-selected-focused-item-icon-color — #262626
--tanrei-comp-navigation-bar-selected-focused-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-selected-focused-item-state-layer-color — #262626
--tanrei-comp-navigation-bar-selected-hovered-item-container-color — #dedede
--tanrei-comp-navigation-bar-selected-hovered-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-selected-hovered-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-selected-hovered-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-selected-hovered-item-icon-color — #262626
--tanrei-comp-navigation-bar-selected-hovered-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-selected-hovered-item-state-layer-color — #262626
--tanrei-comp-navigation-bar-selected-item-container-color sys.color.secondary-container #eeeeee
--tanrei-comp-navigation-bar-selected-item-covered-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-navigation-bar-selected-item-covered-label-color sys.color.on-secondary-container #262626
--tanrei-comp-navigation-bar-selected-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-selected-item-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-navigation-bar-selected-item-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-navigation-bar-selected-item-state-layer-color comp.navigation-bar.selected.item.icon.color #262626
--tanrei-comp-navigation-bar-selected-pressed-item-container-color — #dadada
--tanrei-comp-navigation-bar-selected-pressed-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-selected-pressed-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-selected-pressed-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-selected-pressed-item-icon-color — #262626
--tanrei-comp-navigation-bar-selected-pressed-item-label-color — #1b1b1b
--tanrei-comp-navigation-bar-selected-pressed-item-state-layer-color — #262626
--tanrei-comp-navigation-bar-unselected-disabled-item-container-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-disabled-item-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-unselected-disabled-item-covered-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-unselected-disabled-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-disabled-item-icon-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-unselected-disabled-item-label-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-unselected-disabled-item-state-layer-color — #1b1b1b · 38%
--tanrei-comp-navigation-bar-unselected-focused-item-container-color — #4b4b4b · 10%
--tanrei-comp-navigation-bar-unselected-focused-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-unselected-focused-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-unselected-focused-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-focused-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-focused-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-focused-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-hovered-item-container-color — #4b4b4b · 8%
--tanrei-comp-navigation-bar-unselected-hovered-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-unselected-hovered-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-unselected-hovered-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-hovered-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-hovered-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-hovered-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-item-container-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-item-covered-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-navigation-bar-unselected-item-covered-label-color sys.color.on-secondary-container #262626
--tanrei-comp-navigation-bar-unselected-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-item-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-navigation-bar-unselected-item-label-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-navigation-bar-unselected-item-state-layer-color comp.navigation-bar.unselected.item.icon.color #4b4b4b
--tanrei-comp-navigation-bar-unselected-pressed-item-container-color — #4b4b4b · 10%
--tanrei-comp-navigation-bar-unselected-pressed-item-covered-icon-color — #262626
--tanrei-comp-navigation-bar-unselected-pressed-item-covered-label-color — #262626
--tanrei-comp-navigation-bar-unselected-pressed-item-icon-indicator-color — #000000 · 0%
--tanrei-comp-navigation-bar-unselected-pressed-item-icon-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-pressed-item-label-color — #4b4b4b
--tanrei-comp-navigation-bar-unselected-pressed-item-state-layer-color — #4b4b4b
--tanrei-comp-navigation-bar-item-label-typescale sys.typescale.label-small 500 12px/1.3333

测试样例

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

生成自 specs/navigation-bar.spec.json, tokens/comp/navigation-bar.tokens.json, design/tokens/

指南

何时使用

在 compact 屏幕(例如竖持的手机)上使用导航栏,在三到五个顶级目的地之间切换:也就是用户一整天都会反复回到的地方,例如收件箱、搜索和个人资料。导航栏留在每一个顶级屏幕上,并标明用户身在何处。

要在一个屏幕之内切换同一主题的不同视图,请使用标签页。要离开一个从另一个屏幕进入的屏幕,请使用应用栏的返回按钮:详情屏幕通常会隐藏导航栏。

在导航栏与导航抽屉之间选择

方面 导航栏 收起的导航抽屉 展开的导航抽屉
所在位置 沿底边 沿起始侧边缘的一列图标 起始侧边缘的一块面板里
窗口 Compact Medium 及更宽 Expanded 及更宽(标准),或 compact(模态)
目的地 三到五个 它的顶级目的地和分组,各占一个单元格 产品有多少就放多少,按分区和分组组织
一个目的地 图标在上,单行标签在下,图标上可带徽标 在图标之后截断的行:只有图标,图标上可带徽标,名称放在工具提示里 一行:可选的图标、可换行的标签,以及末尾的后置文本或内容
它还容纳 没有别的 页眉:菜单按钮和主操作,后者收起为它的图标 页眉:菜单按钮、标题或账户切换器,以及主操作
在原生平台上,目的地是 标签栏的一个标签 带选中状态的按钮 带选中状态的按钮
它是什么 布局区域 布局区域,或布局中的一列,模态抽屉从这一列展宽 标准:布局区域。模态:模态浮层
  • 收起的导航抽屉就是导航轨道(navigation rail)。 可收起的抽屉在内容需要宽度时收窄为一列图标,它的菜单按钮再把它展宽;每个图标都留在原位。Tanrei 原先独立的导航轨道已并入这种形态。
  • 导航栏和导航抽屉是同一组目的地的两种摆放位置。 它们接收相同的目的地和相同的选择,所以随窗口自适应的产品可以在某个断点把导航栏换成导航抽屉,而不必改动目的地。一次只显示其中一个,绝不同时显示。
  • 导航抽屉容纳得更多。 当产品的目的地多到导航栏放不下,或者它们可以归成几组(例如邮件应用的文件夹和标签)时,使用展开的导航抽屉。在大窗口上,标准抽屉位于内容旁边。在 compact 窗口上,模态抽屉由菜单按钮唤出,从内容上方滑入。
  • 可收起的模态抽屉把它那一列图标留在布局中;菜单按钮打开它时,它在遮罩之上、内容上方展宽。
  • 在每个平台上,两者都按目的地的值选择(Compose 用每个项上的标志,Flutter 用索引),并触发 change 和 reselect。

标签

labelVisibility 效果 适用于
always(默认) 每个目的地都绘制标签。 大多数导航栏。有了标签,目的地一眼就能认出来。
selected 只有当前目的地绘制标签。没有标签的图标位于其目的地的正中。任何目的地都不会随选择改变大小或位置。 空间紧张、图标又广为人知的导航栏。
never 所有目的地都不绘制标签。 媒体和社交类产品,它们的四五个图标人人都认识。

没有绘制出来的标签仍然是目的地的无障碍名称,所以每个目的地都需要标签。

选择与徽标

  • 活动指示器。 每个目的地都有一个框:它在导航栏宽度中平分到的那一份,并以同一个内缩量(默认系统中为 4)从导航栏的边缘——上、下和两端——向内收进。在一个宽 360、高 64 的导航栏里,四个目的地各有一个 88 × 56 的框,在默认系统中是胶囊形。同一个导航栏中,无论里面放了什么,每个目的地的框都一样大,绝不按图标和标签来决定大小。当前目的地的框会被填充,它就成了活动指示器,其图标也以填充样式显示。每个目的地的悬停、聚焦和按下都画在这同一个框里。
  • 滑动。 选择改变时,指示器保持完整的大小,从旧目的地滑到新目的地。图标和标签只在被指示器盖住的地方反色,所以指示器途经的目的地会在它下面短暂变色,而指示器到位时,新目的地已完全反色。从没有选择变为有选择时,指示器在新目的地上淡入;变为没有选择时,它淡出。它从不放大或缩小。导航栏初次出现时,或者当前目的地在没有新选择的情况下改变了大小(字体加载完成、文本大小变化),指示器会立即就位。在减弱动态效果时,变化立即完成。
  • 拖动。 在某个目的地处于选中状态时,用触摸、触控笔或鼠标按住导航栏并横向移动,指示器就会跟着指针走。松开之前不会选中任何东西:value、焦点、各图标的填充样式以及朗读的内容都留在当前目的地上。松开时,会选中指示器所处的两个目的地中较近的那个,或快速滑动所朝向的那个,并触发一次 change。如果松开时回到了当前目的地,则什么都不触发,reselect 也不触发。Escape 取消拖动。拖动永远不是唯一的方式:轻点、点击、Enter 和 Space 仍像以前一样进行选择。
  • 再次选择。 再次选择当前目的地会触发 reselect,而不是 change。通常的响应是把那个屏幕滚动到顶端,或回到它的第一页。
  • 徽标。 带计数的徽标显示数字(超过 999 时显示 999+),不带计数的显示圆点。badgeLabel 是屏幕阅读器在目的地名称之后朗读的内容,例如“3 封未读”。圆点没有数字可读,所以它总是需要这个标签。目的地被禁用时,徽标仍然保留。

链接

在由一个个独立页面组成的网站里,给每个目的地一个 href,并把导航栏的 value 设为页面自己的地址。这样,当前目的地和当前页面就是同一件事,只用一个标记。

  • 跟随,而不是选中。 对链接的普通激活会触发 change(在当前目的地上则是 reselect),带着目的地的值和它的 href,随后浏览器才跟随链接。导航栏不写入 value:活动指示器留在 value 所指的目的地上,value 改变时才移动。
  • 路由。 取消这个事件,就可以自己接管导航:前往 detail.href,并设置 value。指示器随即滑到新的目的地。
  • 其他标签页。 带修饰键的点击或 Enter、中键点击、target 为 new 的链接,以及下载,都交给浏览器处理。导航栏什么也不触发,什么也不选中。
  • 新上下文。 target: new 打开新的标签页或窗口。目的地是图标在上、标签在下,没有放标记的位置,所以不绘制标记:targetLabel 把这一点告诉辅助技术,位于徽标的描述之后。
  • 拖动。 从链接上开始的横向拖动移动的是活动指示器,不会把链接的地址拖出来。在某个链接上松开,就等于在那个链接上的一次普通按下:指示器回到当前目的地,直到 value 改变。
  • 禁用。 禁用的链接无论用什么方式都无法打开,并且退出焦点顺序。

tonal 与 filled

variant 当前目的地 适用于
tonal(默认) 柔和的指示器,使用 secondary container 颜色,图标和标签使用该容器的内容颜色。 大多数导航栏。
filled 强烈的指示器,使用 primary 颜色,其上的图标和标签反色显示。 必须从内容繁杂的屏幕中凸显出来的导航栏:地图、照片网格、播放器。

没有被指示器盖住的目的地在两种变体中外观相同。变体改变的是选择标示得有多强,而不是朗读的内容:tonal 和 filled 的导航栏暴露相同的名称、角色和状态。被禁用的当前目的地保留它的 tonal 指示器,其上是禁用样式的内容;filled 指示器则像禁用的 filled Button 那样变淡,因为 primary 颜色上的禁用内容会难以辨认。

m3-like 和 cupertino 预设不包含 filled,因为 Material 的导航栏和 Apple 的标签栏都只有一种样式。m3-like 保留 Material 的做法:框只在图标背后,标签在它下方,目的地四周没有框;cupertino 只用它的着色来标示当前标签。在这两个预设下,拖动时看不到任何东西移动,但松开时仍会进行选择。

推荐

  • 标签只用一个简短的词:“收件箱”“搜索”“个人资料”。目的地容纳不下的标签会在末尾以省略号截断,任何内容都不换行。
  • 只有当 tonal 指示器会淹没在导航栏背后的屏幕中时,才使用 filled。
  • 在每个顶级屏幕上保持相同的目的地和相同的顺序。
  • 每个目的地使用相同的图标风格,并让导航栏为当前目的地绘制填充样式。

避免

  • 不要用导航栏放“新建”或“分享”这样的操作。请使用主操作按钮或应用栏。
  • 不要在里面放少于三个或多于五个目的地。调试构建会报告这个数量。把其余的移到导航抽屉里。
  • 不要隐藏有时不可用的目的地。请禁用它,让导航栏保持原有的形态。
  • 不要同时在每个目的地上都显示徽标。

无障碍

角色
navigation
标注
Each destination is named by its label, drawn or not; its icon is decorative, and so is the covered copy of its icon and label. A badge adds its description (badgeLabel, or the displayed count) after the name, and a link that opens a new context its targetLabel after that. The bar's label names the bar itself.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab Moves focus to the next destination that is not disabled: each destination is a tab stop, as the links of a navigation region are.
Enter / Space Activates the focused destination (Enter only for a destination that is a link: it follows the link, and Space is left to the platform).
Enter with a modifier key, on a link (the web) What the browser does with a link: another tab or window, a download. The bar emits nothing and selects nothing (NVB-31).
Escape Cancels a pointer drag of the active indicator in progress: the indicator returns and nothing is selected. Otherwise it does nothing.

备注

  • The semantics follow each platform's idiom for a bar of top-level destinations. On the web it is a navigation landmark whose destinations are links (or buttons in an app that does not change the URL) with aria-current="page" on the current one: that is how a web user recognises site navigation, and the tablist pattern would promise tab panels and arrow-key movement that a switch of whole screens does not have. On iOS, Android, Flutter and React Native the same bar is the platform's tab bar (UITabBar, BottomNavigationView and Material's NavigationBar all expose their items as tabs), so each destination is announced as a tab with its selected state and position.
  • Selected and the current page are one thing. The bar lists the product's top-level places and the selected one is the place being shown, which the web exposes as aria-current="page" on a button and on a link alike. A site of separate pages changes who says which it is — the page, through value, not a press — and nothing else: a link destination is followed, the bar never selects it by itself, and no destination is ever selected and another current.
  • Screen readers announce, for example, "Inbox, tab, 1 of 4, selected, 3 unread": name, role, position, state, then the badge's description.
  • Labels do not wrap: a bar's height is part of the screen's layout. A label longer than its item container allows is truncated, and stays whole as the destination's name. At 200% text size the bar and its item containers grow, and iOS shows the large-content viewer on a long press, as its own tab bar does.
  • The variant changes how strongly the selection is drawn, not what is announced: a tonal and a filled bar expose the same names, roles and states.
  • The drag is never the only way to choose (WCAG 2.5.7): a tap, a click, Enter and Space remain, and the keyboard is unchanged. While a drag lasts nothing a destination exposes changes and nothing is announced; the release announces once, as an activation does. With a screen reader running a one-finger drag is the reader's, and activation works as before.
  • How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

生成自 specs/navigation-bar.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 在 Web 上,导航栏是一个由 label 命名的导航地标。每个目的地是链接(带 href)或按钮,当前目的地带有 aria-current="page",链接和按钮都是如此:“当前页面”没有第二种状态。它不是标签页列表:在整屏之间切换,既没有标签面板,也不用方向键移动。
  • 在 iOS、Android、Flutter 和 React Native 上,导航栏就是平台的标签栏,与它们自带的底部栏一样。每个目的地是一个元素,带有标签角色(在 SwiftUI 上是带选中特征的按钮)、选中状态和所在位置,无论标签是否绘制,都以标签命名。屏幕阅读器会朗读,例如:“收件箱,标签,第 1 个,共 4 个,已选中,3 封未读”。
  • 图标对辅助技术隐藏。徽标会在名称和状态之后加上它的描述(badgeLabel,或它显示的数字),会打开新上下文的链接再在其后加上它的 targetLabel。
  • 每个未禁用的目的地都是独立的 Tab 停靠点,与导航区域中的链接一样。Enter 和 Space 激活获得焦点的目的地(对 Web 上的链接只有 Enter)。禁用的目的地不接受焦点,并朗读为已禁用。
  • 焦点环绘制在目的地的框周围,并限制在导航栏之内,只在键盘聚焦时显示。
  • 拖动指示器在松开之前不会改变任何朗读的内容,也从不移动焦点。
  • 指示器盖住的每个图标和标签的反色副本只是装饰:辅助技术对每个目的地只会遇到一次,variant 也不改变朗读的内容。
  • 整个单元格都是点击区域:导航栏的全部高度和平分到的宽度,至少达到最小目标。导航栏如果窄到放不下它的目的地,仍会平分宽度:单元格会收窄到目标尺寸以下,而不是让导航栏超出它所在的空间。
  • 文本变大时,导航栏会随之变大,每个目的地的框也一样,使标签仍能排在一行里。图标不会缩放。被截断的标签仍完整地作为目的地的名称。在 iOS 提供大内容查看器(large content viewer)的地方,长按会放大显示目的地的图标和标签。
  • 调试构建会报告:目的地少于三个或多于五个,目的地没有标签,值不对应任何目的地,以及同一个值被使用了两次。它们还会报告:会打开新上下文却没有 targetLabel 的链接、指向脚本或无法解析的 href,以及导航栏没有定义的 target。

已知的平台差距:

  • SwiftUI、Compose、Flutter 和 React Native 没有链接目的地:在这些平台上,目的地还不接受 href。
  • Web:所有尺寸都跟随 rem,因此调大浏览器的文本大小时,项的容器和图标也会变大。只放大 Tanrei 的文字则不会。
  • SwiftUI:单元格的最小尺寸是平台自己的目标尺寸,在 macOS 上是 44 点,而规格指定的是 48 点的触摸目标。
  • SwiftUI:在 iOS 以外,目的地把它的位置作为值文本(“2 of 4”)携带,而且只有英文。图像图标(例如 SF Symbol)不会跟随选择变化:请自己为选中的目的地传入填充样式的符号。大内容查看器已经内建,但还没有测试覆盖它。
  • Compose 没有导航栏级别的值,所以“值不对应任何目的地”这种情况无从报告:不把任何项标记为 selected,就是不选中任何一个。
  • Flutter 的语义中没有“在集合中的位置”,所以不会朗读“第 1 个,共 4 个”。
  • SwiftUI、Flutter 和 React Native 会立即切换图标的字体族和填充样式,而不是随选择动效逐渐过渡。
  • React Native 没有大内容查看器。目的地把它的位置作为值文本(“1 of 4”)携带,除非你传入 positionText,否则是英文。
  • React Native:选择动效、边衬区以及 TalkBack 和 VoiceOver 朗读的内容,尚未在真机上验证。
  • 所有平台,默认系统:tonal 指示器相对于导航栏的对比度约为 1.15:1,iOS 的无障碍审计会把它标出来。填充样式的图标和标签的颜色同样标示着选择;在这一点要紧的地方,filled 提供对比度至少 3:1 的指示器。

需要你做的事

  • 给每个目的地一个标签,即使导航栏不绘制任何标签。
  • 用 label 给导航栏命名,例如“主导航”,屏幕上还有别的导航区域时尤其如此。
  • 给每个徽标一个 badgeLabel,说明它统计的是什么;圆点则一定要有。
  • 让每个目的地的值保持唯一且稳定。
  • 在由一个个独立页面组成的网站里,在每个页面上把 value 设为该页面对应的目的地。导航栏不会自己选中链接。
  • 给在新标签页中打开的链接一个用你的产品语言写的 targetLabel,例如“在新标签页中打开”。Tanrei 不自带任何文字。
  • 在 Compose 上,只把一个项标记为 selected,或者一个也不标记。在 Flutter 上,把收到的索引与 selectedIndex 比较,以区分再次选择和选择变更。
  • 在 React Native 上,传入用你的产品语言写的 positionText,并把窗口的边衬区交给 TanreiProvider。