标签页
用轨道上一行带标签的标签页,在同一主题的多个视图之间切换。其中一个标签页处于选中状态,由指示器标出;指示器在标签页之间滑动,也会跟随在这一行上拖动的手指。
- 类别
- 导航
- 令牌
comp.tabs- 层级
- 基础
- 用于
- Tab view
- Web
tanrei-tabsschedule - SwiftUI
TanreiTabsschedule - Jetpack Compose
TanreiTabsschedule - Flutter
TanreiTabBarschedule - React Native
Tabsschedule
- 不提供的变体
- m3-expressive-like: tonal · m3-like: tonal
生成自 docs/status/status.json, specs/tabs.spec.json
规格已修订,尚未覆盖所有平台
本页描述的是标签页现在的规格:标签页位于轨道之上,只有一个指示器,它会滑向选中的标签页,也会跟随在这一行上的拖动;标签页默认贴合自己的内容;标签过长时截断而不换行;多行标签页可以共享同一个选择并记住它(group 和 rememberAs)。fullWidth 取代了 scrollable。一行标签页还有三种变体(主要、次要和柔和)、两种尺寸、可以省去的轨道,以及说明指示器是否滑动的 transition。Web 组件做到了这一切,演示也是如此。SwiftUI、Compose、Flutter 和 React Native 在上方标记为“待跟进”:在跟上之前,它们提供的是较早的一版,有 scrollable(在 Flutter 上是默认为 false 的 isScrollable)、线形指示器、宽度固定且会换行的标签,没有轨道,也不能拖动。
主要标签页,以及选中标签页的面板
三个服务,全部在运行。
2 小时前已部署。
区域、域名和访问权限。
交互演示
一行标签页只有一种项:tab。各平台接收这些标签页的方式,与它接收任何重复子项的方式相同:在 Web 上是 <tanrei-tab> 子元素,在 SwiftUI 的视图构建器中是用值做 tag 的 TanreiTab 视图,在 Compose 的内容 lambda 中是对 TanreiTab 的调用,在 Flutter 的 tabs: 列表中是 TanreiTab widget,在 React Native 上是由普通对象组成的 items 数组。
选择属于这一行,并且按值来记录。value 保存选中标签页的值,change 携带新的值。它在 SwiftUI 上写作 selection,在 Compose 上写作 selectedValue。留空时,选中第一个可用的标签页。标签页的 disabled 属于它自己。
标签页不绘制面板,但由它决定显示哪一个。在 Web 上,<tanrei-tab-panel> 元素跟在标签页之后,并通过 value 与它们对应。在 SwiftUI 上,每个标签页带着自己的面板。在 Flutter 上,TanreiTabBarView 显示选中值对应的面板。在 Compose 和 React Native 上,由你自己紧接在这一行之后显示选中的面板。如果面板需要让用户在其间轻扫切换,请改用标签页视图:它会绘制自己的一行标签页,或者关联到放在别处(例如应用栏里)的一行标签页。
<!-- 这一行选中的标签页就是它的 value,和 <select> 一样。 -->
<tanrei-tabs value="overview" label="项目">
<tanrei-tab value="overview" label="概览" icon="dashboard"></tanrei-tab>
<tanrei-tab value="activity" label="动态" icon="history"></tanrei-tab>
<tanrei-tab value="billing" label="账单" icon="payments" disabled></tanrei-tab>
<!-- 面板跟在标签页之后,并通过 value 与它们对应。 -->
<tanrei-tab-panel value="overview">…</tanrei-tab-panel>
<tanrei-tab-panel value="activity">…</tanrei-tab-panel>
<tanrei-tab-panel value="billing">…</tanrei-tab-panel>
</tanrei-tabs>
<!-- 指定同一个 group 的各行显示同一个选择。remember-as 用你自己的键把它保存在设备上。 -->
<tanrei-tabs variant="secondary" group="units" remember-as="report-units" label="单位">
<tanrei-tab value="metric" label="公制"></tanrei-tab>
<tanrei-tab value="imperial" label="英制"></tanrei-tab>
<tanrei-tab-panel value="metric">…</tanrei-tab-panel>
<tanrei-tab-panel value="imperial">…</tanrei-tab-panel>
</tanrei-tabs>
<script type="module">
import '@tanrei/web/components/tabs';
// 这一行先设置自己的 value,再触发 change;change 携带这个值。
document.querySelector('tanrei-tabs').addEventListener('change', (e) => showView(e.detail.value));
</script>import { Tab, TabPanel, Tabs } from '@tanrei/react';
<Tabs value={view} label="项目" onChange={(e) => setView(e.detail.value)}>
<Tab value="overview" label="概览" icon="dashboard" />
<Tab value="activity" label="动态" icon="history" />
<TabPanel value="overview"><Overview /></TabPanel>
<TabPanel value="activity"><Activity /></TabPanel>
</Tabs>
// 指定同一个 group 的各行显示同一个选择;只有用户操作的那一行会触发 onChange。
<Tabs variant="secondary" group="units" rememberAs="report-units" label="单位" onChange={(e) => setUnits(e.detail.value)}>
<Tab value="metric" label="公制" />
<Tab value="imperial" label="英制" />
<TabPanel value="metric"><Metric /></TabPanel>
<TabPanel value="imperial"><Imperial /></TabPanel>
</Tabs>import Tanrei
// 和 TabView 带 tag 的标签页一样:每个标签页带着自己的面板,只显示选中的那一个。
// 写入绑定就相当于 change。selection 为 nil(Binding<V?>)时,选中第一个可用的标签页。
// .tag(_:) 和 .labelStyle(.iconOnly) 要直接加在 TanreiTab 上,先于其他任何修饰符。
@State private var view = "overview"
TanreiTabs(selection: $view, label: "项目") {
TanreiTab("概览", icon: "dashboard") { OverviewView() }.tag("overview")
TanreiTab("动态", icon: "history") { ActivityView() }.tag("activity")
TanreiTab("账单", icon: "payments") { BillingView() }.tag("billing").disabled(true)
}import io.github.chnotchy.tanrei.components.TanreiTab
import io.github.chnotchy.tanrei.components.TanreiTabs
import io.github.chnotchy.tanrei.primitives.TanreiText
// 状态提升:selectedValue 由你保存,并在 onSelectedValueChange 中更新。
var view by rememberSaveable { mutableStateOf("overview") }
Column {
TanreiTabs(selectedValue = view, onSelectedValueChange = { view = it }, contentDescription = "项目") {
TanreiTab(value = "overview", label = "概览", icon = { TanreiText("dashboard") })
TanreiTab(value = "activity", label = "动态", icon = { TanreiText("history") })
// enabled 是规格中 disabled 的反义。
TanreiTab(value = "billing", label = "账单", icon = { TanreiText("payments") }, enabled = false)
}
// Compose 没有标签页面板角色:按阅读顺序,把选中的视图显示在这一行的下方。
when (view) {
"overview" -> Overview()
"activity" -> Activity()
}
}import 'package:tanrei/tanrei.dart';
// 和 TabBar 一样,但按值选中,而不是按 TabController 的索引。这个栏自己不保存选择。
TanreiTabBar<String>(
value: view, // 为 null 时选中第一个可用的标签页
onChanged: (String value) => setState(() => view = value),
semanticLabel: '项目',
tabs: const <TanreiTab<String>>[
TanreiTab<String>(value: 'overview', label: '概览', icon: Icon(AppSymbols.dashboard)),
TanreiTab<String>(value: 'activity', label: '动态', icon: Icon(AppSymbols.history)),
TanreiTab<String>(value: 'billing', label: '账单', icon: Icon(AppSymbols.payments), enabled: false),
],
),
// 显示选中值对应的视图,不支持轻扫。每个视图都是一个标签页面板,以对应标签页的标签命名。
TanreiTabBarView<String>(
value: view,
semanticLabels: const <String, String>{'overview': '概览', 'activity': '动态', 'billing': '账单'},
children: <String, Widget>{'overview': const OverviewPage(), 'activity': const ActivityPage(), 'billing': const BillingPage()},
),import { useState } from 'react';
import { Tabs } from '@tanrei/react-native';
const [view, setView] = useState('overview');
// 受控:在 onValueChange 中更新 value。
<Tabs
accessibilityLabel="项目"
value={view}
onValueChange={setView}
items={[
{ value: 'overview', label: '概览', icon: 'dashboard' },
{ value: 'activity', label: '动态', icon: 'history' },
{ value: 'billing', label: '账单', icon: 'payments', disabled: true },
]}
/>
// React Native 没有标签页面板角色:紧接在这一行之后渲染选中的面板。
{view === 'overview' ? <Overview /> : <Activity />}次要标签页、填满的一行、贴合内容并可滚动的一行,以及纯图标的一行
柔和变体的标签页:带轨道的、不带轨道的,以及小号、不带轨道且没有过渡的
构成
构成
primary 之上。| 部件 | 说明 |
|---|---|
1 tablist | The row that holds the tabs. It lays them out on the track, hugging their content (scrolling when they do not fit) or sharing its width, moves keyboard focus among them, and follows a drag across it with the active indicator. |
2 track | The constant background of the whole row, a capsule in the default system: one color per variant, the same whichever tab is selected and whatever state a tab is in, with no outline. Tabs are one choice among equals, so the track never says which one is selected; the active indicator does. It may be left out (track): the row then stands on the surface it is placed on and keeps the track's inset, corners and clip, so that nothing moves. A layer whose tabs sit on a line makes it transparent. |
3 container | One tab's state layer, the area that draws hover, focus and press: the box the active indicator has on this tab while it is selected, or would have if it were. In the default system that is a capsule, or a circle where the tab's content is not wider than it is tall, and in a row that fills it is as wide as the tab; it is never the tab's rectangle. The area that answers hover and a tap is still the whole tab. |
4 icon · 可选 | A glyph above the label (a primary tab of the medium size) or before it (a secondary or tonal tab, and every tab of the small size). |
5 label · 可选 | The tab's text and its accessible name, always on one line. A tab that hugs its content shows it whole; a tab that shares the row's width truncates it with an ellipsis and shows it whole where the platform can. It may be hidden for an icon-only tab (labelHidden) and still name it. |
6 active-indicator | The thumb: the shape behind the selected tab's content — filled in primary and secondary tabs, a translucent layer in tonal ones — and, with the selected label's color, the mark of the selection in the default system. Its height is the row's less the track's inset, the same on every tab, and on the first and the last tab it lies that same inset from the track's end, so the thumb's round end and the track's are concentric at every row height. In a row that hugs it is as wide as the selected tab's content plus padding, and a circle where that content is not wider than it is tall (an icon, one full-width character, a one-letter label); in a row that fills it is as wide as the tab. It slides to the newly selected tab, or is drawn on it at once where the row's transition is none, and follows a finger dragged across the row, and the label and icon take their covered colors wherever it covers them, so their inversion travels with it. |
7 underline | The line on the row's bottom edge under the selected tab: as wide as the tab's content in primary tabs, as wide as the tab in secondary tabs. It moves with the active indicator. The default system leaves it transparent; a layer that marks the selection with a line (Material's tabs) colors it. |
8 divider | The hairline along the bottom of the whole row, under the underline. The default system leaves it transparent, because its row is a track and needs no line; a layer whose tabs sit on a line colors it. |
9 panel | The view a tab shows. The tabs associate the selected tab with it and hide the others; they do not draw it. A row's panels are its own or, where a Tab view draws the row or is linked to it, the Tab view's, which switches and swipes them. A row may have none: it then switches what the product draws below it, and says so by its name alone (TAB-42). |
10 focus-ring | Gap-then-ring indicator shown for keyboard focus, drawn around the tab's container, so that it surrounds the place the active indicator has on that tab (a capsule or a circle in the default system), and held inside the track. |
尺寸
尺寸
| 长度 | 令牌 | 值(px) |
|---|---|---|
container padding-inline | tanrei.comp.tabs.container.padding-inline.medium | 16 |
container height | tanrei.comp.tabs.container.height.medium | 48 |
active-indicator height | tanrei.comp.tabs.active-indicator.height | 32 |
primary 之上。属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
variant | primary | secondary | tonal | "primary" | How much the selection weighs. Primary tabs switch the main views of a screen, at the top of its content: the active indicator is the filled color. Secondary tabs switch within one of those views: the indicator is one step quieter. Tonal tabs are the quiet row, for a choice that should not compete with what it changes — the language of a code sample, a contrast, a unit, a period: the indicator is a tonal layer, under a label that is darker than the others. The three are the same control in every other respect (TAB-50), and each is drawn with or without its track (track), at each size (size). |
size | small | medium | "medium" | The row's height (32 / 48) and what shrinks with it: a tab's padding, the active indicator's margins and the icon. Medium is the row of a screen's views, as tall as the touch target. Small stands in a header or a toolbar, as tall as a small Button; its hit area still meets the minimum target (TAB-5), and its icon stands before the label in every variant. The label's text style, the colors and the motion are the same at both. |
track | boolean | null | Whether the row draws its track, the tonal background behind all its tabs. False draws none: the row stands on the surface it is placed on — in a header that is already a bar, on a card, in the heading of a sample — and nothing else changes, neither its size nor a tab's place nor a color of its variant (TAB-49). True draws it. Null, the default, takes comp.tabs.track.visible, the system's choice, which is on in the default system. Independent of variant: each of the three is drawn both ways. |
fullWidth | boolean | false | False (hug, the default) sizes each tab to its content, never truncating its label, and scrolls the tabs sideways inside the track when they do not fit; the active indicator's width then follows the selected tab's content. True (fill) divides the row's width equally among the tabs and truncates a label that does not fit its share (for two to five short labels); the active indicator's width is then the tab's, the same on every tab. |
orientation | horizontal | vertical | "horizontal" | The direction the tabs run. Horizontal (the default) is a row above the views it switches. Vertical stacks the tabs in a column at the start edge, beside the views — the categories of a settings page or of a dialog, the sections of one record — with every tab as wide as the column and its icon before its label (TAB-51). It switches views of one subject as a row does; it is not navigation between the destinations of an app, which is the navigation drawer's. fullWidth does not apply to a column (TAB-56). |
value | string | null | The value of the selected tab. The tabs set it when a tab is activated, or when a drag across the row is released on another tab, and emit change; the product may also set it. null selects the first enabled tab. While the row is a member of a group (group), the group's choice selects the tab of that value wherever the row has one (TAB-36). |
indicatorOffset | number | 0 | Where the active indicator and the underline are drawn while the Tab view it belongs to is swiped, relative to the selected tab: 0 on it, 1 on the next enabled tab and −1 on the previous one in reading order, and in between for values between. The Tab view sets it; a product leaves it at 0 unless it drives the indicator itself. A drag across the row itself is followed by the tabs (TAB-28) and does not pass through this prop. |
transition | auto | slide | none | "auto" | How this row draws a new selection: the row's part of the effect of switching views. slide: the active indicator and the underline slide and resize to the newly selected tab, the labels' colors changing under them, and a tab out of view is scrolled in with a motion. none: the new selection is drawn whole in one frame — nothing travels and nothing fades (TAB-43). Auto takes comp.tabs.motion.transition, the design system's answer (slide in the default system). Under both a drag across the row still carries the indicator and hover, focus and press are answered the same; reduced motion draws none whatever this says. A Tab view has a transition of its own for its panels, and the row it draws follows it where it is none (TBV-22, TBV-23). |
label | string | "" | The accessible name of the row: what the views are views of, or, for a row without panels, what is chosen (Contrast, Units). Needed when a screen has more than one row of tabs, and always for a row that has no panel, where nothing else says what the row is for (TAB-42). |
group | string | "" | Names a choice this row shares with other rows of tabs, placed by the product or drawn by a Tab view. Rows that name the same group show the same choice: selecting a tab in one selects the tab of the same value in every other row that has one, at once and without moving focus or the page, and a row that has no tab of that value keeps its own (TAB-36 to TAB-38, TAB-40). Empty (the default): the row's choice is its own. The name is the product's and is compared exactly. On a platform where the product keeps the selection in its own state, rows given the same state are the group, and each platform's mapping says how it is spelled. |
rememberAs | string | "" | The key under which a person's choice on this row is kept on their device across pages and visits, and from which it is restored before the row is first drawn (TAB-39). Empty (the default): the row neither reads nor writes any storage; nothing is remembered without a key. The key is the product's own and is used exactly as given, so the product can name it in its privacy notice, read it itself and clear it. Only the chosen tab's value is stored. It needs no group, and a group needs no key: sharing and remembering are separate choices. |
事件
| 事件 | 说明 |
|---|---|
change | The user selected another tab, by activating it or by releasing a drag on it; carries its value, which the row's value now holds. |
条目
该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。
tab
One tab of the row: a label, an optional icon, and the value that selects it.
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value | string | "" | Identifies the tab: the row's value names the selected tab by it, and change carries it. Unique within the row; required. |
label | string | "" | The tab's text and its accessible name. Required, even when labelHidden is true. |
icon | string | "" | The name of an icon (a glyph of the system's icon set). Give every tab of a row an icon, or none. |
labelHidden | boolean | false | Draws the icon alone and keeps label as the accessible name (an icon-only tab). |
disabled | boolean | false | Prevents selecting the tab; the arrow keys skip it. |
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 条目 | tanrei-tab | TanreiTab | TanreiTab | TanreiTab | Tab |
value · 属性 | value | tag | value | value | value |
label · 属性 | label | title | label | label | label |
icon · 属性 | icon | icon | icon | icon | icon |
labelHidden · 属性 | labelHidden | labelStyle | labelHidden | labelHidden | labelHidden |
disabled · 属性 | disabled | disabled | enabled | enabled | disabled |
| 备注 | A child element: <tanrei-tab value="overview" label="Overview" icon="dashboard">, with label-hidden and disabled attributes, as <option> in a <select>. | TanreiTab(_ title:, icon:) { panel }.tag(value) in the row's view builder, as TabView's tagged tabs; .labelStyle(.iconOnly) is labelHidden; disabled follows .disabled(_:). | TanreiTab(value, label, icon, labelHidden, enabled) called in the TanreiTabs { } content lambda. enabled is the inverse flag. | TanreiTab<T>(value:, label:, icon:, labelHidden:, enabled:) widgets in the bar's tabs: list. | The element type of the row's items array: { value, label, icon?, labelHidden?, disabled? }. |
状态
行为
以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。
| ID | 需求 |
|---|---|
TAB-1 | WHEN the user activates an enabled tab that is not selected by pointer, touch, Enter or Space THE tabs SHALL set value to the tab's value and emit change exactly once with it; WHEN the tab is already selected THE tabs SHALL emit nothing. |
TAB-2 | WHILE a tab's disabled is true THE tabs SHALL NOT select it or emit change for it, SHALL skip it with the arrow keys, SHALL NOT make it the Tab stop and SHALL expose the disabled state to assistive technology. |
TAB-3 | WHILE a tab is hovered, keyboard-focused or pressed THE tabs SHALL render its container, label, icon, covered label and icon, active indicator, underline and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. |
TAB-4 | WHEN a tab receives keyboard focus THE tabs SHALL show the focus indicator (the ring or the glow, TAB-10) around the tab's container (TAB-18), concentric with it and in its shape, so that it surrounds the place the active indicator has on that tab, with its inner edge sys.focus.offset outside the container's edge. THE tabs SHALL hold the indicator inside the track, which clips a row that scrolls: WHERE the space above and below the container (comp.tabs.track.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 tabs and the active indicator, without animation; focus gained by pointer or touch SHALL NOT show it. |
TAB-5 | WHERE a tab is narrower than the minimum target THE tabs SHALL make it at least sys.target.touch wide for touch input and sys.target.pointer wide for fine pointers, in layout, because tabs touch their neighbours, so a hit area cannot extend over them. WHERE the row is shorter than the minimum target (the small size) THE tabs SHALL extend each tab's hit area above and below the row, by the same distance on each side, to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing the row's height or the place of anything around it; WHERE that extension lies over the row's own panel or over a control beside the row, a press on something that panel or control would itself answer SHALL go to it and not to the tab. A platform that cannot tell the input type SHALL use sys.target.touch. |
TAB-6 | WHILE a tab's disabled is true THE tabs SHALL render its container, label, icon, covered label and icon, active indicator, underline and opacity from the disabled component tokens of its variant and, for the selected tab, of its selected parts. |
TAB-7 | WHEN variant or size names a value that the active design system does not have THE tabs SHALL render with the tokens of that prop's default value, and in debug builds report the unknown value. |
TAB-8 | WHEN the tab is pressed by pointer, touch, Enter or Space THE tab SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner. |
TAB-9 | WHILE pressed THE tab SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; label, icon, covered label and icon, active indicator, underline, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.tabs.motion.feedback-duration with comp.tabs.motion.state-easing, fades over comp.tabs.motion.state-duration with comp.tabs.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. |
TAB-10 | THE tab SHALL ease its container and track 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. |
TAB-11 | THE tabs SHALL render the tab whose value equals the row's value (or, while the row's value is null, the first enabled tab) from the selected parts of its variant (comp.tabs.primary.selected.label.color and the other selected. parts for primary, and so on) and every other tab from the variant's own parts; WHEN value names no tab THE tabs SHALL render none as selected, show no panel, and in debug builds report the value; WHEN a tab's value is empty or repeats another tab's THE tabs SHALL still render it, treat the first tab with a value as the one that value selects, and in debug builds report the tab. |
TAB-12 | THE tabs SHALL be one Tab stop, the selected tab; WHILE orientation is horizontal and a tab has keyboard focus Right Arrow and Left Arrow SHALL move focus to the next and previous enabled tab, wrapping at the ends, and Home and End to the first and last enabled tab, and each move SHALL also select the tab it reaches and emit change (automatic activation); in a right-to-left layout Left Arrow SHALL move to the next tab and Right Arrow to the previous; WHILE orientation is vertical the arrow keys SHALL move and select as TAB-54 says, and WHERE the row has no panel, in either orientation, as TAB-42 says; in either orientation Tab SHALL move focus from the selected tab into its panel, or, WHERE the row has no panel (TAB-42), to whatever follows the row. |
TAB-13 | WHILE the row has a panel THE tabs SHALL expose the tab list role named by the row's label (over a name the product gives the row on the element, aria-label or aria-labelledby, which names it WHILE label is empty), each tab with the tab role, its selected state, its position and the number of tabs, named by its own label whether or not that label is drawn; THE tabs SHALL expose the selected tab's panel with the tab panel role, named by its tab and associated with it where the platform can express that, SHALL hide the other panels from view and from assistive technology, and SHALL make a panel with no focusable content itself focusable so that Tab reaches it. WHERE the tabs belong to a Tab view, as the row it draws itself (TBV-16) or as a row it is linked to (TBV-17), its panels take the place of the tabs' own (TBV-9, TBV-15), and each tab SHALL be associated with the Tab view's panel of its value. A row that has no panel is not a tab list: it is exposed as the single choice it is, as TAB-42 says. |
TAB-14 | WHILE fullWidth is false THE tabs SHALL size each tab to its content plus the padding of the row's size on each side (comp.tabs.container.padding-inline.medium for medium, comp.tabs.container.padding-inline.small for small; TAB-44), or, WHERE its active indicator is a circle (TAB-16), to that circle and no wider, in either case never narrower than its active indicator or than the minimum target (TAB-5), keep its label on one line and whole however long it is or however large the text, place the tabs so that the first tab's active indicator starts comp.tabs.track.padding in from the track's start edge and the last tab's ends the same distance before the track's end (TAB-34), and scroll them sideways inside the track when they are wider than the space the row has; WHILE fullWidth is true THE tabs SHALL divide the row's width, less comp.tabs.track.padding at each end, equally among the tabs, each at least as wide as the minimum target (the tabs scroll sideways only when the row is too narrow for that), keep each label on one line, and truncate a label wider than its tab less the padding of the row's size on each side (and, beside an icon, less the icon and comp.tabs.container.gap) at the end of the label's own direction (TAB-33), with an ellipsis. A label SHALL NOT wrap onto a second line or break within a word, and an icon SHALL NOT shrink to make room for it. |
TAB-15 | WHEN the row first appears with its selected tab partly or wholly out of view, or a tab becomes selected or receives keyboard focus while part of it is scrolled out of view, THE tabs SHALL scroll the tabs inside the track until the whole tab is visible (at once when the row first appears), over comp.tabs.motion.indicator-duration with comp.tabs.motion.indicator-easing, and at once where motion is reduced. WHERE the tab becomes selected because the row follows its group's choice THE tabs SHALL bring it into view at once (TAB-38). WHILE the row's transition is none (TAB-43) THE tabs SHALL bring a tab that becomes selected into view at once. |
TAB-16 | THE tabs SHALL draw the selected tab's active indicator behind its content and centred on it, as tall as the row less comp.tabs.track.padding above and below, which is the same height on every tab of the row whatever the tab holds. WHILE fullWidth is false the indicator SHALL be as wide as the tab's content (its label and icon as TAB-18 lays them out, the icon alone while the label is hidden) plus the active indicator's padding of the row's size on each side (comp.tabs.active-indicator.padding-inline.medium for medium, comp.tabs.active-indicator.padding-inline.small for small), never wider than the tab; WHERE that content is not wider than it is tall (an icon alone, one full-width character, a one-letter label) the indicator SHALL instead be exactly as wide as it is tall, so that it is a circle. WHILE fullWidth is true the indicator SHALL be as wide as the tab, whatever the tab holds. In either case it SHALL NOT be narrower than it is tall, its width SHALL follow the same rule in every variant, and its four corners SHALL be rounded by comp.tabs.primary.active-indicator.shape for primary, comp.tabs.secondary.active-indicator.shape for secondary and comp.tabs.tonal.active-indicator.shape for tonal, each at most half its height. THE tabs SHALL also draw the selected tab's underline on the bottom edge of the row, above the divider, comp.tabs.primary.underline.thickness thick for primary, comp.tabs.secondary.underline.thickness for secondary and comp.tabs.tonal.underline.thickness for tonal, with the corners that face away from the divider rounded by comp.tabs.primary.underline.shape for primary, comp.tabs.secondary.underline.shape for secondary and comp.tabs.tonal.underline.shape for tonal, spanning the tab's content for primary and the tab's whole width for secondary and tonal. Both are drawn in every system, each in its look's color; a transparent color (the default system's underline, and the active indicator of a system that marks the selection with the line) hides the part without changing the layout. The content the indicator's width is measured from SHALL be the label at the heavier of its two weights (TAB-50), so that no tab, indicator or container changes width when the selection moves. A tab with no content (an empty label and no icon, which TAB-22 reports in debug builds) has an underline of no width and a circular active indicator; the report, not a wider indicator, is the remedy. |
TAB-17 | WHEN the selected tab changes while indicatorOffset is 0 and no drag is followed (TAB-28), and the row's transition is slide (TAB-43), THE tabs SHALL slide and resize the active indicator and the underline together from the old tab to the new one over comp.tabs.motion.indicator-duration with comp.tabs.motion.indicator-easing, drawn in the newly selected tab's colors, every frame of the slide being the in-between look of TAB-28 (the indicator as a drag would draw it at that place: its width between the two tabs' indicators, a circle becoming a capsule and a capsule a circle, each label and icon in its covered color exactly where it is covered), and SHALL move them at once where motion is reduced; the panel SHALL change at once unless a linked Tab view switches it (TBV-2). WHEN the row first appears, or the selected tab changes size or place without a new selection (a font arriving, the text size changing, the row resizing), THE tabs SHALL put the active indicator and the underline on it at once, without a slide. WHERE the selected tab changes because the row follows a choice of its group that was made on another row, or one restored from storage, THE tabs SHALL likewise put them on it at once (TAB-38). |
TAB-18 | THE tabs SHALL make every tab of a row as tall as the row: at least the container height of the row's size (comp.tabs.container.height.medium for medium, comp.tabs.container.height.small for small), at least comp.tabs.container.height-with-icon where a primary tab stacks an icon above a drawn label, and tall enough that the content of its tallest tab keeps the indicator's padding of that size (comp.tabs.active-indicator.padding-block.medium for medium, comp.tabs.active-indicator.padding-block.small for small) between itself and the active indicator's top and bottom edges (TAB-16); each tab's content SHALL be centred in it and clear of the underline's thickness at the bottom edge. THE tabs SHALL draw each tab's container, which carries hover, focus and press, in the box the active indicator has on that tab while it is selected, or would have if it were (TAB-16), centred in the tab: as tall as the row less comp.tabs.track.padding above and below; WHILE fullWidth is false as wide as the tab's content plus the active indicator's padding of the row's size on each side (TAB-16), and exactly as wide as it is tall WHERE that content is not wider than it is tall; WHILE fullWidth is true as wide as the tab. Its corners SHALL be rounded by comp.tabs.container.shape, each at most half its height, and THE tabs SHALL NOT draw hover, focus or press on the part of the tab's rectangle outside that box. The container SHALL lie above the track and under the active indicator, except on the selected tab, whose container SHALL lie above the active indicator and under the focus indicator, drawn from that tab's selected parts (its state layer is comp.tabs.primary.selected.state-layer.color, comp.tabs.secondary.selected.state-layer.color or comp.tabs.tonal.selected.state-layer.color by variant, the color of the content the indicator covers), so that hover and press show on the selected tab as on any other; the area that answers hover and a press SHALL remain the whole tab (TAB-5): a pointer anywhere on the tab shows the state, in the container. A primary tab with an icon and a drawn label SHALL place them as the icon placement of the row's size says (comp.tabs.icon.placement.medium for medium, comp.tabs.icon.placement.small for small, and a size a layer added by its own value, or medium's WHERE the layer declares none): stacked puts the icon above the label, comp.tabs.container.gap apart; inline puts it before the label, comp.tabs.container.gap apart, centred on the label's line. A secondary or tonal tab SHALL place its icon before its label at every size, as inline does. THE tabs SHALL draw the icon at the icon size of the row's size (comp.tabs.icon.size.medium for medium, comp.tabs.icon.size.small for small) through the icon helper with the family and fill of the tab's selection (selected for the selected tab, otherwise unselected), sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and the label in comp.tabs.label.typescale. |
TAB-19 | WHILE a tab's labelHidden is true THE tabs SHALL draw only the tab's icon and keep its label as its accessible name; WHILE labelHidden is true and icon is empty THE tabs SHALL draw the label anyway and in debug builds report the tab. |
TAB-20 | THE tabs SHALL draw a line of comp.tabs.divider.thickness in comp.tabs.divider.color along the bottom of the whole row, under the tabs, the active indicator and the underline, across the full width of the row even while its tabs scroll, and SHALL hide it from assistive technology. The line is drawn in every system; a transparent color (the default system's) hides it without changing the layout. |
TAB-21 | WHEN the layout direction is right-to-left THE tabs SHALL order the tabs from the right, start the track and a row of tabs that hug their content at the right edge, truncate a label at the end of the label's own direction (TAB-33: a right-to-left label at its left end, a left-to-right label at its right end), and mirror the slide of the active indicator and the underline; a drag (TAB-28) SHALL still carry them to where the pointer is; WHILE orientation is vertical the column SHALL still run from top to bottom, its start edge being its right one: each tab's icon and label SHALL start at the tab's right edge, and the divider and the underline SHALL lie on the column's left edge (TAB-53). |
TAB-22 | WHILE a tab's label is not empty THE tabs SHALL expose it as the tab's accessible name, over a name the product gives the tab on the element (aria-label or aria-labelledby); WHILE a tab's label is empty and the product gives the tab a name of its own on the element — aria-label, or aria-labelledby naming an element — THE tabs SHALL leave that name to the tab; WHILE a tab's label is empty and the product gives it none THE tabs SHALL still render the tab and in debug builds report that it has no accessible name. |
TAB-23 | WHEN the colors of a tab change between states or between selected and not selected THE tabs SHALL animate them over comp.tabs.motion.state-duration with comp.tabs.motion.state-easing, and SHALL change them at once where motion is reduced. WHERE a tab becomes selected or stops being selected because the row follows a choice of its group that was made on another row, or one restored from storage, THE tabs SHALL change its colors at once (TAB-38). WHILE the row's transition is none THE tabs SHALL change the colors of a tab that becomes selected or stops being selected at once (TAB-43), and SHALL still animate a change between states. WHEN the row first appears THE tabs SHALL draw every tab's colors, its label's weight (TAB-50) and its icon's fill (TAB-18) at once, as they are in that first frame, without a motion; WHEN a tab is added to a row that is already drawn THE tabs SHALL draw that tab so too. An appearance is not a change between states. |
TAB-24 | THE tabs SHALL draw each tab's label and icon a second time, over the first, in the covered colors of the tab's look (comp.tabs.primary.covered-label.color and comp.tabs.primary.covered-icon.color for a primary tab that is not selected, comp.tabs.primary.selected.covered-label.color and comp.tabs.primary.selected.covered-icon.color for the selected one, and the same parts of secondary and of tonal), clipped to the active indicator's current shape, its corners included, wherever the indicator is drawn; WHILE the indicator slides (TAB-17) the clip SHALL move with it in the same frame, so that each label and icon it passes over changes where it is covered and the newly selected tab's content is wholly in its covered colors when the indicator lands, neither before nor after. THE tabs SHALL NOT approximate the covered colors with a blend mode, and SHALL expose each label to assistive technology once. |
TAB-25 | WHILE a tab's label is truncated (TAB-14) THE tabs SHALL keep the whole label as the tab's accessible name and SHALL show the whole label through the platform's idiom for revealing truncated text where it has one — a plain tooltip on pointer hover and keyboard focus, or on long press where tooltips open that way, or the platform's large-content viewer — without exposing it to assistive technology a second time; WHILE the label fits THE tabs SHALL show no such tooltip. |
TAB-26 | WHILE indicatorOffset is not 0 THE tabs SHALL draw the active indicator and the underline between the selected tab and the next enabled tab (above 0) or the previous enabled tab (below 0) in reading order, each edge at the linear interpolation between its place on the one tab and on the other by the offset's magnitude, clamped to 1, in the selected tab's colors, and SHALL follow the offset in the frame it changes, without a motion of their own; the covered label and icon SHALL follow as TAB-24 says; WHERE there is no enabled tab in the offset's direction THE tabs SHALL draw them on the selected tab. WHEN value changes while indicatorOffset is not 0 THE tabs SHALL place them at once by the new selected tab and the offset, without the slide of TAB-17. WHILE indicatorOffset is not 0 THE tabs SHALL take no drag of their own (TAB-28): the swipe that sets it already has the indicator. |
TAB-27 | THE tabs SHALL draw a track behind the whole row, under the tabs, their containers and the active indicator: filled, WHILE the row's track is on (TAB-49), with comp.tabs.primary.track.color for primary, comp.tabs.secondary.track.color for secondary and comp.tabs.tonal.track.color for tonal, its corners rounded by comp.tabs.track.shape, each at most half its height, with no outline, as tall as the row, and as wide as the tabs plus comp.tabs.track.padding at each end but never wider than the space the row has; WHILE the tabs do not fit that space THE tabs SHALL keep the track still and scroll the tabs inside it, clipped to its shape (TAB-14). THE track SHALL be the same whichever tab is selected, whatever state any tab is in and while the active indicator moves, because tabs are one choice among equals: only the active indicator marks the selection. THE tabs SHALL hide the track from assistive technology. The track is drawn in every system; a transparent color hides it without changing the layout. So does a row whose track is off (TAB-49): it fills nothing and keeps the track's box, inset, corners and clip. |
TAB-28 | WHEN the user presses the row 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, THE tabs SHALL take the drag: the press SHALL activate no tab when it ends (TAB-1), the tab under it SHALL leave its pressed state as it does when a press ends (TAB-8), and from then until the drag ends THE tabs SHALL draw the active indicator and the underline where the pointer is along the row — on an enabled tab while the pointer is at that tab's centre, halfway between two neighbouring enabled tabs while the pointer is where they meet (the middle of any disabled tabs between them), each edge at the linear interpolation in between, and on the first or last enabled tab while the pointer is before or beyond its centre — in the selected tab's colors. Everything that differs between the indicator on the one tab and on the other SHALL change continuously with the pointer, with no threshold: its place, its width (between the two tabs' indicators, a circle widening into a capsule and a capsule narrowing into a circle), the underline's place and width with it, and the color of each label and icon, which is its covered color exactly where the indicator covers it and its own color elsewhere, the clip of TAB-24 moving with the indicator in the same frame. THE tabs SHALL place them in the frame the pointer moves, without a motion of their own; WHEN the drag is taken with the pointer away from where they rest THE tabs SHALL bring them to the pointer's place over comp.tabs.motion.state-duration with comp.tabs.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 vertical scrolling for its whole length, and a wheel or a trackpad SHALL only scroll. WHILE the tabs do not fit and scroll inside the track (TAB-14) THE tabs SHALL take only a drag that starts on the selected tab, SHALL leave every other sideways drag to scroll the tabs as before, and SHALL scroll the tabs to keep the active indicator in view while a drag carries it toward an end of the track. |
TAB-29 | WHEN the user releases a drag the tabs have taken THE tabs SHALL choose one enabled tab: 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 row, 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 tabs 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 tab is not the selected one THE tabs SHALL set value to the tab's value and emit change exactly once with it, and SHALL slide and resize the active indicator and the underline from where the release left them onto that tab over comp.tabs.motion.indicator-duration with comp.tabs.motion.indicator-easing, as TAB-17 does from a tab; WHERE it is the selected tab THE tabs SHALL return them to it over the same motion and emit nothing. The place where two tabs meet decides only what a release chooses; nothing drawn changes at it. The chosen tab SHALL become the row's Tab stop (TAB-12), and the release SHALL NOT also activate the tab under the pointer. |
TAB-30 | WHEN a drag the tabs have 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 tab the indicator lies between — THE tabs SHALL return the active indicator and the underline to the selected tab (the one the product has just set, where it set one) over comp.tabs.motion.indicator-duration with comp.tabs.motion.indicator-easing and emit nothing. WHILE a drag lasts THE tabs SHALL NOT change value, the panel shown (their own, or a Tab view's, whose shown panel cannot change before a tab is chosen), keyboard focus, or the selected state that each tab exposes to assistive technology, whatever the indicator covers, and SHALL announce nothing: the selection is made, and announced, by the release alone (TAB-29). WHERE motion is reduced THE tabs SHALL still draw the active indicator and the underline where the pointer is, which the person's own finger moves, taking the drag without the catch-up motion of TAB-28, and SHALL put them on their tab at once when the drag is released or cancelled. |
TAB-31 | THE tabs 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 tabs is shown THE tabs SHALL draw itself for the new direction at once, without a motion of its own, keeping the selected tab, scrolled into view as TAB-15 says, and its focus. |
TAB-32 | WHERE the layout direction is right-to-left THE tabs SHALL draw a tab's icon, in both of its copies (TAB-24) 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 TAB-21 gives. THE tabs 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. |
TAB-33 | THE tabs SHALL lay out a tab's label, in both of its copies (TAB-24) 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 tabs 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 (TAB-14) 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. |
TAB-34 | THE tabs SHALL hold the active indicator the same distance, comp.tabs.track.padding, from the track's top, from its bottom and, on the first and the last tab, from the track's end: at every height the row takes (comp.tabs.container.height.small, comp.tabs.container.height.medium, comp.tabs.container.height-with-icon, or taller with larger text), whether the tabs hug or fill, and whether the indicator is a capsule or a circle; so that wherever the track's corner radius less comp.tabs.track.padding is the indicator's corner radius — at every height while comp.tabs.track.shape and the variant's active-indicator shape are both full, as in the default system — the indicator's end and the track's end are concentric curves. WHERE the first or the last tab is wider than its active indicator (a circle in a tab widened to the minimum target) THE tabs SHALL let that tab reach into the track's inset on that side by half the difference, as far as the track's edge, rather than hold the indicator further in; WHERE half the difference is more than the inset the tab SHALL end at the track's edge in layout and its hit area SHALL extend beyond the track's end as far as the minimum target needs, without affecting layout. A tab's container and its focus ring are drawn in the indicator's box and SHALL lie in the same place; WHILE the tabs scroll inside the track the distance SHALL hold at each end once the row is scrolled to that end. All of this SHALL hold whether the track is drawn or not (TAB-49). |
TAB-35 | WHILE fullWidth is false and every tab of the row draws its icon alone (TAB-19) THE tabs SHALL draw every tab's place — the active indicator on the selected tab, the container of each other tab — as a circle of one diameter, the row's height less comp.tabs.track.padding above and below, with the tab's glyph, the icon size of the row's size across (TAB-18), at its center; SHALL set neighbouring circles' centers one tab's width apart, every tab being as wide as the larger of the circle and the minimum target of TAB-5; SHALL make the track as wide as that leaves it and no wider, the first and the last circle concentric with its ends (TAB-34), so that the row is symmetric about its middle and, but for the thumb's place and the glyphs, the same whichever tab is selected; SHALL keep the active indicator that circle, neither wider nor narrower, in every frame while it slides or is dragged from one tab to another (TAB-17, TAB-28); and SHALL give the tabs hit areas that meet halfway between neighbouring circles and reach the track's top, its bottom and, on the first and the last tab, its end, so that every point of the track selects the tab whose circle is nearest and every hit area meets the minimum target. A row of two such tabs stays a row of tabs: each is exposed as a tab with its label as its name, and activating the selected one changes nothing. |
TAB-36 | WHILE group is not empty THE tabs SHALL be a member of the group of that name, together with every other row of tabs in the same document or window whose group is the same string, compared exactly, whether the product placed that row or a Tab view drew it (TBV-16); a group SHALL hold one choice, the value of a tab, or none, and SHALL hold none until TAB-37 or TAB-39 gives it one. WHILE its group holds a choice and the tabs has an enabled tab with that value THE tabs SHALL have that tab selected: it SHALL set value to the choice and emit nothing, in the first frame it is drawn, in the frame the choice changes, and in the frame it gains such a tab, has such a tab enabled, or is given the group. WHILE its group holds no choice, or the tabs has no enabled tab with the choice's value, THE tabs SHALL keep its own value and select as TAB-11 says, SHALL report nothing, and SHALL follow the group again as soon as the choice is the value of one of its enabled tabs. WHILE group is empty THE tabs SHALL share its choice with no other row. |
TAB-37 | WHEN a person selects a tab on a row that is a member of a group (TAB-1, TAB-12, TAB-29), or the product sets value on such a row, after the row has first been drawn, to the value of one of its enabled tabs, THE tabs SHALL make that value its group's choice in the same frame, so that every member follows as TAB-36 says. THE row the person selected on SHALL emit change exactly once, as it does without a group, and every other member SHALL emit nothing, so that each choice a person makes is announced once, by the row it was made on, however many rows follow it; a value set by the product SHALL emit nothing on any member. The value a row has when it is first drawn SHALL NOT become the group's choice, nor SHALL a value that names none of the row's tabs or names a disabled one; a drag across a row before its release (TAB-28) and indicatorOffset SHALL NOT be shared, and the other members wait for the selection. |
TAB-38 | WHEN a group's choice gives a row another selected tab and that row is neither the one a person selected on nor the one the product set (TAB-37), or the choice is one restored from storage (TAB-39), THE tabs SHALL draw that tab as selected at once: the active indicator and the underline on it without the slide of TAB-17, its colors without the motion of TAB-23, the tab brought into view inside the track at once (TAB-15), and its own panel changed at once (TAB-13). THE tabs SHALL NOT move keyboard focus to that row or within it, SHALL NOT show a hovered, focused or pressed state for the change, SHALL NOT scroll anything outside its own track to bring itself into view, and SHALL announce nothing. THE row a person selected on SHALL answer the press, slide and keep keyboard focus as it does without a group, and a row whose value the product set SHALL slide as TAB-17 says. Every member SHALL draw the new choice in the same frame: no frame SHALL show one member with the new choice and another member, which has an enabled tab of that value, with the old one. WHERE a member follows a drag of its own when the choice changes THE tabs SHALL cancel that drag as TAB-30 says for a value the product sets. |
TAB-39 | WHILE rememberAs is not empty THE tabs SHALL keep the person's choice on their device: WHEN a person's selection gives the row another selected tab, made on the row itself or, through its group, on another member (TAB-37), THE tabs SHALL store that tab's value, and nothing else, under exactly the key rememberAs in the storage the platform keeps for the product on the device across pages and visits, once for each key however many members name it; a value set by the product and a choice restored from storage SHALL be stored by no row. WHEN the row is first drawn while a value is stored under its key THE tabs SHALL restore it before that first frame: WHILE the row is a member of a group that holds no choice the stored value SHALL become the group's choice, whether or not this row has a tab with it (a group that already holds a choice keeps it), and WHILE the row has no group the row SHALL select its enabled tab with that value where it has one; no frame SHALL show the row with another tab selected, or another panel, first, and the row SHALL emit nothing. THE tabs SHALL use a stored value only to compare it with its tabs' values, and SHALL NOT draw it, announce it or run it. WHILE rememberAs is empty THE tabs SHALL neither read nor write any storage. IF the storage is missing, blocked or full, or reading or writing it fails, THEN THE tabs SHALL behave as though nothing were stored, keep the choice for as long as the document or window lasts, and raise and report nothing. |
TAB-40 | WHEN a person selects a tab on a row that is a member of a group and, in following that choice, other members change the size of what lies before the row in a region that scrolls, THE tabs SHALL keep the row where the person is using it: in the frame that first draws the new choice the row SHALL be at the place in the window that it had in the frame before, the scroll positions of the regions it scrolls in having been moved by exactly the distance the row would otherwise have moved, at once, with no scrolling motion and with no frame in between that shows the row elsewhere; WHERE a region cannot scroll that far THE tabs SHALL move it as far as it goes. THE tabs SHALL make no such correction for a choice the product set or one restored from storage, where no person has a place to keep, and SHALL leave what lies after the row to move as its own panel's and the other members' new heights require. |
TAB-41 | WHILE many rows are members of one group (a page of sixty code samples) THE tabs SHALL do the work of a choice once for the group and not once for each row: storage SHALL be read at most once for each key in the life of a document or window and written at most once for each key and choice (TAB-39), the place of the row acted on SHALL be measured and corrected once (TAB-40), and a member that follows SHALL run no motion (TAB-38). WHEN a member is removed THE tabs SHALL take it out of its group, and the group SHALL keep its choice for as long as the document or window lasts, with or without members, so that a row drawn later (a view navigated to without a new document) takes it in its first frame. |
TAB-42 | WHILE the row has no panel — none of its own, none of a Tab view that draws it or is linked to it (TAB-13), and none of a component that draws the row together with the panel it switches (the date-time picker's entry-mode tabs, DTM-13) — THE tabs SHALL expose the row as a single choice and not as a tab list: the radio group role named by the row's label, and each tab with the radio role, its checked state (the selected tab checked, every other tab not), its disabled state, its position and the number of tabs, named by its own label whether or not that label is drawn; THE tabs SHALL expose no tab list, tab or tab panel role, SHALL associate no tab with any element as the thing it controls, and SHALL show and hide nothing itself. THE tabs SHALL keep the row one Tab stop, the selected tab (TAB-12), Tab moving focus from it to whatever follows the row. WHILE a tab of such a row has keyboard focus, in either orientation, Right Arrow and Down Arrow SHALL move focus to the next enabled tab and Left Arrow and Up Arrow to the previous one, wrapping at the ends, with Right Arrow and Left Arrow swapped in a right-to-left layout, and Home and End SHALL move it to the first and the last enabled tab; each move SHALL select the tab it reaches and emit change, as the arrows of a radio group check the radio they reach, and Enter and Space SHALL select the focused tab (TAB-1). A selection SHALL be made, announced and emitted exactly as on a row with panels: the checked state is what assistive technology is told, and what the choice changes below the row is the product's to draw on change. WHEN the row gains its first panel or loses its last THE tabs SHALL change what they expose to match, at once, without changing the selection, keyboard focus or anything drawn. WHERE a row has a panel for some of its tabs and not for others it is a tab list (TAB-13): THE tabs SHALL associate each tab that has one with it and SHALL show no panel while a tab without one is selected. |
TAB-43 | THE tabs SHALL draw a new selection as the row's transition names: the name transition gives, or, WHILE transition is auto, the name comp.tabs.motion.transition holds (slide or none); WHEN transition names a value the tabs do not define THE tabs SHALL treat it as auto and in debug builds report it. WHILE the row's transition is slide, TAB-15, TAB-17, TAB-23 and TAB-28 to TAB-30 hold as written. WHILE it is none THE tabs SHALL draw a new selection whole in the frame it is made, whatever made it — a press, a key, the release of a drag, the product, or the row's group: the active indicator and the underline on the newly selected tab at their full size and gone from the tab that was selected, each of the two tabs' labels and icons in the colors of its new look, and the tab brought into view inside the track (TAB-15), with no slide, no resize, no fade and no frame that shows a look in between. THE tabs SHALL still take a drag across the row (TAB-28) and still draw the active indicator where indicatorOffset puts it (TAB-26), each in the frame the pointer or the offset moves, since the person's own finger moves it and no motion is the component's; they SHALL take the drag without the catch-up motion of TAB-28 and SHALL put the active indicator and the underline on their tab at once when the drag is released or cancelled (TAB-29, TAB-30), as TAB-30 says where motion is reduced. A tab's hover, focus and press SHALL be answered as under slide: the state's colors over comp.tabs.motion.state-duration (TAB-23), the press as sys.behavior.pressed-feedback names and held for sys.motion.duration.press-hold (TAB-8, TAB-9), the focus indicator without animation (TAB-4); WHERE the newly selected tab is still hovered, focused or pressed it SHALL be drawn in that state of the selected look. WHERE motion is reduced THE tabs SHALL draw a new selection as none does whatever transition names. Changing transition SHALL NOT change the selected tab, keyboard focus or what the row emits, and a slide that is running when the row's transition becomes none SHALL end on its tab at once. |
TAB-44 | THE tabs SHALL draw the row at its size: every tab at least the container height of that size tall (comp.tabs.container.height.medium for medium, comp.tabs.container.height.small for small), with the tab padding (comp.tabs.container.padding-inline.medium for medium, comp.tabs.container.padding-inline.small for small), the active indicator's margins (comp.tabs.active-indicator.padding-inline.medium for medium, comp.tabs.active-indicator.padding-inline.small for small; comp.tabs.active-indicator.padding-block.medium for medium, comp.tabs.active-indicator.padding-block.small for small) and the icon size (comp.tabs.icon.size.medium for medium, comp.tabs.icon.size.small for small) of that size. Everything else SHALL be the same at every size: the label's text style (comp.tabs.label.typescale), the gap, the track's inset and corners (comp.tabs.track.padding, comp.tabs.track.shape), every color and every motion, what assistive technology is told and the keyboard model. The heights are minimums (TAB-18): with larger text a small row grows as a medium one does, cuts nothing, and scrolls its tabs inside the track rather than the page (TAB-14). WHILE size is small THE tabs SHALL place a tab's icon before its label in every variant (TAB-18) and SHALL meet the minimum target by its hit area (TAB-5). Size, variant, track and transition are independent: each variant SHALL be drawn at each size, and changing size SHALL NOT change the selected tab, keyboard focus or what is scrolled into view. |
TAB-45 | WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE tabs SHALL draw the active indicator as a filled, opaque shape in the platform's highlight color with the selected tab's label and icon in its highlighted-text color, every other tab's label and icon in its text color, a disabled tab's in its color for disabled text and a disabled selected tab's indicator in that same color, the underline in the highlight color and the divider in the text color, and the focus indicator in the platform's own colors, in every variant alike and with or without the track. THE tabs SHALL NOT rely on the track, on a state layer, on a translucent fill or on a difference between two of the system's own colors to say which tab is selected there: the filled indicator SHALL remain, so that a tonal row without a track, whose indicator is a faint layer in the system's own colors, shows its selection as plainly as a primary one. |
TAB-49 | THE tabs SHALL fill the track (TAB-27) while the row's track is on and SHALL fill nothing behind the row while it is off: on or off as track says while it is true or false, and, WHILE track is null, as comp.tabs.track.visible holds (on in the default system). WHILE the track is off the surface the row is placed on SHALL show between and around the tabs, and THE tabs SHALL change nothing else: the row's size, the inset the track has (comp.tabs.track.padding) above, below and at each end, the place and size of every tab, container, active indicator, underline and focus indicator (TAB-4, TAB-34), the corners that clip a row that scrolls (comp.tabs.track.shape, TAB-14), the hit areas, and every color of the row's variant, so that turning the track on or off moves nothing and two rows that differ only in it line up. THE tabs SHALL draw every part in its own color over whatever lies beneath it — the track, or with no track whatever is behind the row — and SHALL NOT assume a surface: a color that is translucent (a state layer, the default system's tonal active indicator) is laid over what is there. The track is decoration, drawn or not, and nothing a person is told changes with it. Track, variant, size and transition are independent: each variant SHALL be drawn with and without its track at each size, and changing track SHALL be drawn at once, without a motion of its own, and SHALL NOT change the selected tab, keyboard focus, what is scrolled into view or what the row emits. |
TAB-50 | THE tabs SHALL draw the three variants alike in everything but their tokens: the same parts in the same places, the same sizes, the same keyboard model, and the same roles, names and states told to assistive technology; a variant says how much the selection weighs, not what the row is. Each variant SHALL read its own parts (comp.tabs.primary.selected.active-indicator.color, comp.tabs.secondary.selected.active-indicator.color, comp.tabs.tonal.selected.active-indicator.color, and so on for every part TAB-3, TAB-6, TAB-11 and TAB-24 name). WHERE an active indicator's color is translucent — the default system's tonal indicator is a layer of the selected tab's content color — THE tabs SHALL draw it once, at that opacity, over the track or the surface beneath it, with the selected tab's state layer (TAB-18) over it; SHALL NOT let two drawings of it overlap and darken while it slides, resizes or is dragged; and SHALL draw the covered label and icon (TAB-24) over it at their own opacity. A tonal tab's label and icon are comp.tabs.tonal.label.color and comp.tabs.tonal.icon.color where the indicator does not cover them and comp.tabs.tonal.selected.covered-label.color and comp.tabs.tonal.selected.covered-icon.color on the selected tab once the indicator has landed, so that in the default system a tonal row tells its selection by the layer and by the darker label together. THE selected tab's label SHALL be drawn, in both of its copies, at the weight of comp.tabs.label.typescale plus comp.tabs.primary.selected.label.emphasis, comp.tabs.secondary.selected.label.emphasis or comp.tabs.tonal.selected.label.emphasis by variant, so that a selection drawn by a faint indicator is also told by something other than color; every other tab's label SHALL keep the typescale's weight, and every tab SHALL keep the width its label needs at the heavier weight whether it is selected or not, so that nothing in the row moves or wraps when the selection does. The weight SHALL change with the label's color (TAB-23), at once where the colors do. A tonal tab with an icon and a drawn label SHALL place the icon before the label at every size (TAB-18). |
TAB-51 | WHILE orientation is vertical THE tabs SHALL stack the tabs from top to bottom in the order given, in a column at the start edge of the row beside its panels, every tab as wide as the column: the widest tab's content plus the padding of the row's size on each side (comp.tabs.container.padding-inline.medium for medium, comp.tabs.container.padding-inline.small for small; TAB-44), or the width the product gives the tab list where that is wider, and never wider than the space the row has. Each tab's icon SHALL stand before its label in every variant and at every size, comp.tabs.container.gap apart, both at the tab's start edge, and each tab SHALL be at least the container height of the row's size tall and, because the tabs touch their neighbours above and below, at least the minimum target tall in layout (sys.target.touch for touch input, sys.target.pointer for fine pointers; TAB-5). TAB-51 to TAB-61 SHALL hold in place of what TAB-5, TAB-14, TAB-16, TAB-18, TAB-20, TAB-27, TAB-28, TAB-34 and TAB-35 say of a horizontal row's widths, heights, insets and edges; every other behaviour holds in both orientations. |
TAB-52 | WHILE orientation is vertical THE tabs SHALL draw the selected tab's active indicator, and every tab's container (TAB-18), as the tab's box less comp.tabs.track.padding on every side: as wide as the column less the inset on each side and as tall as the tab less the inset above and below, whatever the tab holds, never a circle, its corners rounded by the variant's active-indicator shape, each at most half its height (TAB-16); and SHALL slide and resize it from one tab to another along the column over comp.tabs.motion.indicator-duration with comp.tabs.motion.indicator-easing, as TAB-17 says, the covered label and icon following it as TAB-24 says. |
TAB-53 | WHILE orientation is vertical THE tabs SHALL draw the divider (TAB-20) as a line of comp.tabs.divider.thickness in comp.tabs.divider.color along the column's end edge, the side of the panels, the full height of the column, and the selected tab's underline (TAB-16) as a line of the variant's underline thickness on that edge, above the divider and as tall as the selected tab in every variant, its corners that face away from the divider rounded by the variant's underline shape; in a right-to-left layout both SHALL lie on the column's left edge. |
TAB-54 | WHILE orientation is vertical, the row has a panel and a tab has keyboard focus, Down Arrow and Up Arrow SHALL move focus to the next and previous enabled tab, wrapping at the ends, and Home and End to the first and last enabled tab, each move also selecting the tab it reaches and emitting change (automatic activation, TAB-12); Right Arrow and Left Arrow SHALL do nothing, in either layout direction, and are left to the page. A column without a panel moves by every arrow key, as TAB-42 says. |
TAB-55 | WHILE orientation is vertical and the row has a panel THE tabs SHALL expose the tab list as vertical (aria-orientation on the web, the platform's equivalent where it has one) and every other role, name, state and relation as TAB-13 says; WHILE it is horizontal THE tabs SHALL expose the tab list's orientation as the platform's default for a tab list, which is horizontal. A row without a panel (TAB-42) SHALL expose no orientation in either case, because its arrow keys do not depend on it. |
TAB-56 | WHILE orientation is vertical and the tabs are taller than the space the row has, THE tabs SHALL scroll them vertically inside the track, keeping the track still and the tabs clipped to its shape (TAB-27), and SHALL bring a tab into view along the column as TAB-15 says; WHILE orientation is vertical fullWidth SHALL be ignored, and WHERE it is true a debug build SHALL report it. |
TAB-57 | WHILE orientation is vertical THE tabs SHALL take a drag (TAB-28 to TAB-30) only from a mouse or a pen moved past the slop further up or down than sideways, carrying the active indicator and the underline along the column as TAB-28 says along the row, and SHALL leave every touch to scroll the tabs or whatever holds the row, and every drag that goes sideways first to the page. |
TAB-58 | WHILE orientation is vertical THE tabs SHALL round each corner of the track by the smaller of comp.tabs.track.shape and the active indicator's corner radius (the variant's active-indicator shape, at most half the indicator's height) plus comp.tabs.track.padding, so that the track's corners are concentric with the indicator's on the first and the last tab however tall the column is. |
TAB-59 | WHILE orientation is vertical and the row is linked to a Tab view (TBV-17) THE tabs SHALL draw indicatorOffset (TAB-26) along the column, between the selected tab and the enabled tab below it (above 0) or above it (below 0); the row a Tab view draws itself (TBV-16) is horizontal. |
TAB-60 | WHEN orientation names a value the tabs do not define THE tabs SHALL lay the tabs out horizontally, and in debug builds report the value. |
TAB-61 | WHILE orientation is vertical and a tab's label is wider than its tab less the padding of the row's size on each side (and, beside an icon, less the icon and comp.tabs.container.gap) — a column narrower than its widest tab — THE tabs SHALL truncate the label at the end of its own direction (TAB-33) with an ellipsis, on one line, and SHALL show it whole as TAB-25 says. |
TAB-62 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the row's host, by the attribute or by the ARIA property that reflects it, THE tabs 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 row's role (its tab list, or its radio group while the row has no panel) — an id reference by the elements it names in the host's tree or a tree around it. 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 tabs SHALL take it off that element. A tab's host carries the tab's role itself, so what the product writes on it is not moved, but for a name: WHILE a tab's label is not empty THE tabs SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the tab's whole name although the platform gives aria-labelledby the precedence (TAB-22), and WHEN label is emptied SHALL give them back as they were written; WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE tabs SHALL take it away. |
各平台名称对照
对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。
| 规格 | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| 组件 | tanrei-tabs | TanreiTabs | TanreiTabs | TanreiTabBar | Tabs |
variant · 属性 | variant | variant | variant | variant | variant |
size · 属性 | size | size | size | size | size |
track · 属性 | track | track | track | track | track |
fullWidth · 属性 | fullWidth | fullWidth | fullWidth | isScrollable | fullWidth |
orientation · 属性 | orientation | orientation | orientation | direction | orientation |
value · 属性 | value | selection | selectedValue | value | value |
indicatorOffset · 属性 | indicatorOffset | link | link | link | link |
transition · 属性 | transition | transition | transition | transition | transition |
label · 属性 | label | label | contentDescription | semanticLabel | accessibilityLabel |
group · 属性 | group | selection | selectedValue | value | value |
rememberAs · 属性 | rememberAs | selection | selectedValue | value | value |
change · 事件 | change | selection | onSelectedValueChange | onChanged | onValueChange |
| 备注 | tanrei-tabs (variant, full-width, value, label) holds tanrei-tab children and, after them, tanrei-tab-panel children matched to tabs by their value attribute, or is drawn by a tanrei-tab-view as its own row, or is named by a linked tanrei-tab-view (tabs="<id>") whose tanrei-tab-panel children take their place. indicatorOffset is a property only, with no attribute: the Tab view writes it in every frame of a drag. value names the selected tab, as a <select>'s does. It sets role tablist, tab and tabpanel, aria-selected, aria-controls and aria-labelledby (radiogroup and radio with aria-checked on a row without panels, TAB-42), hides the panels that are not selected, and gives a panel with no focusable content tabindex 0. It sets its own value before firing change, as a native control does. The covered copy of a label and icon (TAB-24) is an aria-hidden layer clipped with clip-path to the indicator's box, animated with it; a truncated label (text-overflow: ellipsis) shows the whole label in a plain tooltip (TAB-25) without aria-describedby, since it is already the name. The track is the row's own background, and the tabs scroll inside it. A drag (TAB-28 to TAB-30) is followed with Pointer Events: pointerdown on the row, setPointerCapture once the drag is taken, pointermove placing the indicator, pointerup choosing the tab, and pointercancel, lostpointercapture or Escape cancelling; the click that follows a taken drag is swallowed, and aria-selected is written only on the release. The in-between look needs no second mechanism: the indicator's inset and width are the same custom properties a slide transitions, written in each frame of a drag, and the covered copy's clip-path follows them, so a drag and a slide draw the same frames. touch-action: pan-y on a row that does not scroll (and on the selected tab of one that does) leaves sideways drags to the row and vertical ones to the page. A shared choice: group and remember-as are attributes with the properties group and rememberAs. A group is the tanrei-tabs of one document that name it, those inside shadow trees and those a tanrei-tab-view drew included; an iframe is another document. The members and the choice are kept by the module, in a map from the name, and never in the DOM. remember-as is a key of the origin's localStorage, read synchronously before the row's first render and at most once for each key, every access inside try, so a browser that blocks storage or throws on it costs nothing but the memory; a key is the product's to namespace, since localStorage is shared by everything on the origin. A member that follows is written (value, aria-selected, its panels' hidden) with its slide and its color transition off for that frame. For TAB-40 the row acted on reads its getBoundingClientRect().top before the group is written and once more after, and moves scrollTop of its nearest scrolling ancestor (the document's scrolling element where none is nearer) by the difference with behavior: 'instant'; a browser that anchors scrolling itself has by then moved some or all of the way, and the row corrects only what is left, so the two never add up. change bubbles, so one listener on the document hears each choice once and reads the group from event.target.group. The first frame of TAB-39 is the first the element draws: before tanrei-tabs is defined the element draws nothing of its own, and a product that shows server-rendered rows before then guards that as it guards any element not yet defined. The value tonal of variant; size is an attribute and a property (small, medium); both axes are open registries (TanreiTabsVariantRegistry, TanreiTabsSizeRegistry in registry.ts, in the Button's form), and each size's five tokens are read through the same private custom properties, set by a block per size as a variant's are. A small row's tab keeps its 32-high box and carries the hit area of TAB-5 as an inset pseudo-element that reaches above and below it, under any interactive content it overlaps; the row itself is not made taller. track is a property that is true, false or null: the attribute is absent for null, present for true and ="false" for false, as the tooltip's arrow is; null reads --tanrei-comp-tabs-track-visible from the cascade by a style query, as a separation is read, and off sets the track's fill to none and nothing else. transition is an attribute and a property (auto, slide, none); auto reads --tanrei-comp-tabs-motion-transition, in the stylesheet by a style query and once in the script for the scroll and the settle after a drag; under none the indicator's and the cover's transitions and the selected look's color transition are off, scrollIntoView uses behavior: 'instant', and the drag is taken as it is under prefers-reduced-motion. The tonal indicator is one element with a translucent background, so its slide and resize never overlap two drawings of it (TAB-50). A row without panels (TAB-42) has role radiogroup where a row with panels has role tablist, with no aria-orientation, and each tanrei-tab role radio with aria-checked in place of aria-selected and no aria-controls. The row has a panel while it holds a tanrei-tab-panel, while the tanrei-tab-view that draws it or is linked to it has a panel for one of its tabs, or while a Tanrei element that draws the row links its own panel to it (the date-time picker links its time section, DTM-13); the roles follow in the update that re-reads the panels. The roles are read from the panels and never set by the product, so a row cannot claim panels it does not have. | TanreiTabs(selection: Binding<V>, variant:, fullWidth:) { TanreiTab … }: each tab carries its panel, and only the selected one is shown. The binding holds the selected tab's tag; writing it is change. label sets the row's accessibilityLabel. A truncated label (.lineLimit(1), .truncationMode(.tail)) is shown whole by the large-content viewer and by .help on macOS (TAB-25). SwiftUI's TabView is an app's tab bar (a navigation bar), not this component. Until it catches up it ships scrollable: instead of fullWidth:, a line indicator and wrapping fixed labels. link is the shared TanreiTabLink a linked Tab view is passed too: the tabs read the indicator offset (TAB-26) from it and report their order and disabled tabs to the Tab view through it; without a Tab view it is omitted and the offset is 0. The drag is a DragGesture(minimumDistance:) on the row, simultaneousGesture with the tabs' taps, its onChanged placing the indicator and its onEnded reading predictedEndTranslation for the fling; the binding is written only in onEnded. Until it catches up it also draws no track and follows no drag. A shared choice has no parameter here: rows given the same Binding show the same choice, which is the group (TAB-36), and SwiftUI redraws them in one frame. The product keeps a row that lacks a value on a binding of its own, since a selection that names none of a row's tabs selects none (TAB-11), and remembers by backing the binding with @AppStorage (or @SceneStorage), which is restored before the first frame. TAB-37 to TAB-41 describe a group the component keeps, which this platform does not have: every row slides as TAB-17 says for a value the product sets, since the row cannot tell following from being set; the README says so, and says that a ScrollView does not hold a row in place when content above it changes height. Not built on this platform yet: the selected label's added weight from selected.label.emphasis with every tab keeping the heavier label's width (TAB-16, TAB-50); the selected tab's state layer over its active indicator (TAB-18); the icon's place read from icon.placement of the row's size (TAB-18); variant: .tonal; size: TanreiTabsSize (.small, .medium); track: Bool? (nil reads the generated choice trackVisible); transition: TanreiTabsTransition (.auto, .slide, .none; .auto reads the generated choice), under .none the selection is set without withAnimation and the DragGesture stays. The hit area of a small row is .contentShape on a frame of the minimum target. A row whose tabs carry no panel is the single choice of TAB-42: a container named by label without the tab bar trait, each tab a button with the selected trait, as a segmented picker is told; until it catches up every row has the tab bar trait. | TanreiTabs(selectedValue, onSelectedValueChange, variant, fullWidth, contentDescription) { TanreiTab(…) }, one composable for rows that hug and rows that fill (Material's PrimaryTabRow, SecondaryTabRow and ScrollableTabRow in one). State hoisting: the caller keeps selectedValue and moves it from onSelectedValueChange. A truncated label (maxLines = 1, TextOverflow.Ellipsis) is shown whole in a plain tooltip on long press and hover (TAB-25). Compose has no tab panel role: the product shows the selected tab's content below the row. Until it catches up it ships scrollable instead of fullWidth, a line indicator and wrapping fixed labels. link is the shared rememberTanreiTabLink() a linked Tab view is passed too: the tabs read the indicator offset (TAB-26) from it and report their order and disabled tabs to the Tab view through it; without a Tab view it is omitted and the offset is 0. The drag is Modifier.pointerInput with detectHorizontalDragGestures (or draggable with a DraggableState) on the row, a VelocityTracker giving the fling; onSelectedValueChange is called only when the drag ends. Until it catches up it also draws no track and follows no drag. A shared choice has no parameter here: rows given the same hoisted selectedValue show the same choice, which is the group (TAB-36), and recompose in one frame. The caller gives a row that lacks a value a state of its own, since a selection that names none of a row's tabs selects none (TAB-11), and remembers by keeping the state in DataStore or SharedPreferences, read before the first composition. TAB-37 to TAB-41 describe a group the component keeps, which this platform does not have: every row slides as TAB-17 says for a value the product sets, since the row cannot tell following from being set; the README says so, and says that a LazyColumn keeps its first visible item in place when items above change height while a scrolling Column does not. Not built on this platform yet: the selected label's added weight from selected.label.emphasis with every tab keeping the heavier label's width (TAB-16, TAB-50); the selected tab's state layer over its active indicator (TAB-18); the icon's place read from icon.placement of the row's size (TAB-18); TanreiTabsVariant.Tonal; size: TanreiTabsSize (Small, Medium); track: Boolean? = null (null reads the generated choice trackVisible); transition: TanreiTabsTransition (Auto, Slide, None), under None the indicator is placed with snapTo in place of animateTo and the drag is still detected. The hit area of a small row is Modifier.minimumInteractiveComponentSize() outside the drawn box. The row holds no panel here, since the product draws the content below it, so it is the single choice of TAB-42 until a Tab view links one: a selectableGroup() named by contentDescription, each tab with Role.RadioButton and its selected state; until it catches up every tab has Role.Tab. | TanreiTabBar<T>(value:, onChanged:, variant:, isScrollable:, semanticLabel:, tabs: [TanreiTab<T>(…)]), as TabBar but selected by value rather than by a TabController's index; the caller moves value from onChanged. isScrollable is the inverse of fullWidth, as in Flutter's TabBar, and defaults to true (hug). A truncated label (maxLines: 1, TextOverflow.ellipsis) is shown whole in a Tooltip (TAB-25). TanreiTabBarView gives way to the Tab view, TanreiTabView, which draws a TanreiTabBar itself or is linked by link:, a TanreiTabLink<T> passed to both, as Flutter's TabController is shared by a TabBar and a TabBarView: the bar reads the indicator offset (TAB-26) from it and reports its order and disabled tabs to the view through it. Until it catches up, TanreiTabBarView<T>(value:, children:, semanticLabels:) shows the panel of the selected value and does not swipe. Until it catches up it ships isScrollable defaulting to false, a line indicator and wrapping fixed labels. The drag is a GestureDetector's onHorizontalDragStart, onHorizontalDragUpdate and onHorizontalDragEnd on the bar, DragEndDetails.primaryVelocity giving the fling; onChanged is called only from onHorizontalDragEnd. Until it catches up it also draws no track and follows no drag. A shared choice has no parameter here: bars given the same value from one piece of the product's state show the same choice, which is the group (TAB-36), and rebuild in one frame. The product gives a bar that lacks a value a state of its own, since a selection that names none of a bar's tabs selects none (TAB-11), and remembers with shared_preferences or its own store, read before the first build. TAB-37 to TAB-41 describe a group the component keeps, which this platform does not have: every row slides as TAB-17 says for a value the product sets, since the row cannot tell following from being set; the README says so, and says that a scroll view does not hold a bar in place when content above it changes height. Not built on this platform yet: the selected label's added weight from selected.label.emphasis with every tab keeping the heavier label's width (TAB-16, TAB-50); the selected tab's state layer over its active indicator (TAB-18); the icon's place read from icon.placement of the row's size (TAB-18); TanreiTabsVariant.tonal; size: TanreiTabsSize (small, medium); bool? track (null reads the generated choice trackVisible); transition: TanreiTabsTransition (auto, slide, none), under none the controller's value is set without animateTo and the horizontal drag is still recognised. The hit area of a small row is a GestureDetector box of the minimum target with HitTestBehavior.translucent. A row with no linked TanreiTabBarView holds no panel and is the single choice of TAB-42: each tab is a radio, Semantics(checked:, inMutuallyExclusiveGroup: true), in place of SemanticsRole.tab and selected; until it catches up every tab has SemanticsRole.tab. | <Tabs items={Tab[]} value onValueChange variant fullWidth accessibilityLabel>; controlled, the caller moves value from onValueChange and renders the selected panel below the row. React Native has no tab panel role, and the row holds no panel (the caller renders it), so the row is the single choice of TAB-42: the radiogroup role, and each tab the radio role with its checked state. Until it catches up the row has the tablist role and each tab the tab role with its selected state, and the selected panel follows the row in reading order. A truncated label (numberOfLines={1}, ellipsizeMode="tail") keeps the whole label as the tab's accessibilityLabel; React Native has no tooltip of its own, so it is not shown whole elsewhere (TAB-25: where the platform has an idiom). Until it catches up it ships scrollable instead of fullWidth, a line indicator and wrapping fixed labels. link is the shared useTabLink() a linked Tab view is passed too: the tabs read the indicator offset (TAB-26) from it and report their order and disabled tabs to the Tab view through it; without a Tab view it is omitted and the offset is 0. The drag is a PanResponder on the row (or React Native Gesture Handler's Gesture.Pan() where the product has it), claiming the responder once the drag passes the slop sideways, with the release's vx giving the fling; onValueChange is called only on release. Until it catches up it also draws no track and follows no drag. A shared choice has no prop here: rows given the same controlled value from one piece of the product's state show the same choice, which is the group (TAB-36), and render in one commit. The product gives a row that lacks a value a state of its own, since a selection that names none of a row's tabs selects none (TAB-11), and remembers with AsyncStorage or its own store; AsyncStorage answers after the first frame, so a product that must not show another tab first reads it before it mounts the screen. TAB-37 to TAB-41 describe a group the component keeps, which this platform does not have: every row slides as TAB-17 says for a value the product sets, since the row cannot tell following from being set; the README says so, and says that a ScrollView holds a row in place only with maintainVisibleContentPosition. Not built on this platform yet: the selected label's added weight from selected.label.emphasis with every tab keeping the heavier label's width (TAB-16, TAB-50); the selected tab's state layer over its active indicator (TAB-18); the icon's place read from icon.placement of the row's size (TAB-18); variant="tonal"; size ("small", "medium"); track?: boolean | null (null reads the generated choice trackVisible); transition ("auto", "slide", "none"), under "none" the indicator's value is set with setValue in place of Animated.timing and the PanResponder stays. The hit area of a small row is hitSlop. |
令牌
| 令牌 | 引用 | 解析值(浅色) |
|---|---|---|
--tanrei-comp-tabs-active-indicator-height | — | 32px |
--tanrei-comp-tabs-active-indicator-padding-block-medium | sys.space.space50 | 4px |
--tanrei-comp-tabs-active-indicator-padding-block-small | sys.space.space25 | 2px |
--tanrei-comp-tabs-active-indicator-padding-inline-medium | comp.tabs.container.padding-inline.medium | 16px |
--tanrei-comp-tabs-active-indicator-padding-inline-small | comp.tabs.container.padding-inline.small | 12px |
--tanrei-comp-tabs-container-gap | sys.space.space50 | 4px |
--tanrei-comp-tabs-container-height-with-icon | — | 64px |
--tanrei-comp-tabs-container-height-medium | — | 48px |
--tanrei-comp-tabs-container-height-small | — | 32px |
--tanrei-comp-tabs-container-padding-inline-medium | sys.space.space200 | 16px |
--tanrei-comp-tabs-container-padding-inline-small | sys.space.space150 | 12px |
--tanrei-comp-tabs-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-tabs-disabled-opacity | — | 1 |
--tanrei-comp-tabs-divider-color | — | |
--tanrei-comp-tabs-divider-thickness | sys.border.hairline | 1px |
--tanrei-comp-tabs-focused-opacity | — | 1 |
--tanrei-comp-tabs-hovered-opacity | — | 1 |
--tanrei-comp-tabs-icon-placement-medium | — | 0 |
--tanrei-comp-tabs-icon-placement-small | — | 1 |
--tanrei-comp-tabs-icon-size-medium | sys.icon.medium | 24px |
--tanrei-comp-tabs-icon-size-small | sys.icon.small | 20px |
--tanrei-comp-tabs-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-tabs-motion-indicator-duration | sys.motion.role.selection.travel | 250ms |
--tanrei-comp-tabs-motion-indicator-easing | sys.motion.easing.spatial | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-tabs-motion-state-duration | sys.motion.role.state.large | 250ms |
--tanrei-comp-tabs-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-tabs-motion-transition | — | 0 |
--tanrei-comp-tabs-pressed-opacity | — | 1 |
--tanrei-comp-tabs-primary-active-indicator-color | — | |
--tanrei-comp-tabs-primary-active-indicator-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-tabs-primary-active-indicator-thickness | — | 3px |
--tanrei-comp-tabs-primary-container-color | — | |
--tanrei-comp-tabs-primary-covered-icon-color | sys.color.on-primary | |
--tanrei-comp-tabs-primary-covered-label-color | sys.color.on-primary | |
--tanrei-comp-tabs-primary-disabled-active-indicator-color | — | |
--tanrei-comp-tabs-primary-disabled-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-primary-disabled-active-indicator-thickness | — | 3px |
--tanrei-comp-tabs-primary-disabled-container-color | — | |
--tanrei-comp-tabs-primary-disabled-covered-icon-color | — | |
--tanrei-comp-tabs-primary-disabled-covered-label-color | — | |
--tanrei-comp-tabs-primary-disabled-icon-color | — | |
--tanrei-comp-tabs-primary-disabled-label-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-active-indicator-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-container-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-covered-icon-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-covered-label-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-icon-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-label-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-state-layer-color | — | |
--tanrei-comp-tabs-primary-disabled-selected-underline-color | — | |
--tanrei-comp-tabs-primary-disabled-state-layer-color | — | |
--tanrei-comp-tabs-primary-disabled-track-color | — | |
--tanrei-comp-tabs-primary-disabled-underline-color | — | |
--tanrei-comp-tabs-primary-disabled-underline-shape | — | 3px |
--tanrei-comp-tabs-primary-disabled-underline-thickness | — | 3px |
--tanrei-comp-tabs-primary-focused-active-indicator-color | — | |
--tanrei-comp-tabs-primary-focused-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-primary-focused-active-indicator-thickness | — | 3px |
--tanrei-comp-tabs-primary-focused-container-color | — | |
--tanrei-comp-tabs-primary-focused-covered-icon-color | — | |
--tanrei-comp-tabs-primary-focused-covered-label-color | — | |
--tanrei-comp-tabs-primary-focused-icon-color | — | |
--tanrei-comp-tabs-primary-focused-label-color | — | |
--tanrei-comp-tabs-primary-focused-selected-active-indicator-color | — | |
--tanrei-comp-tabs-primary-focused-selected-container-color | — | |
--tanrei-comp-tabs-primary-focused-selected-covered-icon-color | — | |
--tanrei-comp-tabs-primary-focused-selected-covered-label-color | — | |
--tanrei-comp-tabs-primary-focused-selected-icon-color | — | |
--tanrei-comp-tabs-primary-focused-selected-label-color | — | |
--tanrei-comp-tabs-primary-focused-selected-state-layer-color | — | |
--tanrei-comp-tabs-primary-focused-selected-underline-color | — | |
--tanrei-comp-tabs-primary-focused-state-layer-color | — | |
--tanrei-comp-tabs-primary-focused-track-color | — | |
--tanrei-comp-tabs-primary-focused-underline-color | — | |
--tanrei-comp-tabs-primary-focused-underline-shape | — | 3px |
--tanrei-comp-tabs-primary-focused-underline-thickness | — | 3px |
--tanrei-comp-tabs-primary-hovered-active-indicator-color | — | |
--tanrei-comp-tabs-primary-hovered-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-primary-hovered-active-indicator-thickness | — | 3px |
--tanrei-comp-tabs-primary-hovered-container-color | — | |
--tanrei-comp-tabs-primary-hovered-covered-icon-color | — | |
--tanrei-comp-tabs-primary-hovered-covered-label-color | — | |
--tanrei-comp-tabs-primary-hovered-icon-color | — | |
--tanrei-comp-tabs-primary-hovered-label-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-active-indicator-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-container-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-covered-icon-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-covered-label-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-icon-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-label-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-state-layer-color | — | |
--tanrei-comp-tabs-primary-hovered-selected-underline-color | — | |
--tanrei-comp-tabs-primary-hovered-state-layer-color | — | |
--tanrei-comp-tabs-primary-hovered-track-color | — | |
--tanrei-comp-tabs-primary-hovered-underline-color | — | |
--tanrei-comp-tabs-primary-hovered-underline-shape | — | 3px |
--tanrei-comp-tabs-primary-hovered-underline-thickness | — | 3px |
--tanrei-comp-tabs-primary-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-primary-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-primary-pressed-active-indicator-color | — | |
--tanrei-comp-tabs-primary-pressed-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-primary-pressed-active-indicator-thickness | — | 3px |
--tanrei-comp-tabs-primary-pressed-container-color | — | |
--tanrei-comp-tabs-primary-pressed-covered-icon-color | — | |
--tanrei-comp-tabs-primary-pressed-covered-label-color | — | |
--tanrei-comp-tabs-primary-pressed-icon-color | — | |
--tanrei-comp-tabs-primary-pressed-label-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-active-indicator-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-container-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-covered-icon-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-covered-label-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-icon-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-label-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-state-layer-color | — | |
--tanrei-comp-tabs-primary-pressed-selected-underline-color | — | |
--tanrei-comp-tabs-primary-pressed-state-layer-color | — | |
--tanrei-comp-tabs-primary-pressed-track-color | — | |
--tanrei-comp-tabs-primary-pressed-underline-color | — | |
--tanrei-comp-tabs-primary-pressed-underline-shape | — | 3px |
--tanrei-comp-tabs-primary-pressed-underline-thickness | — | 3px |
--tanrei-comp-tabs-primary-selected-active-indicator-color | sys.color.primary | |
--tanrei-comp-tabs-primary-selected-container-color | — | |
--tanrei-comp-tabs-primary-selected-covered-icon-color | sys.color.on-primary | |
--tanrei-comp-tabs-primary-selected-covered-label-color | sys.color.on-primary | |
--tanrei-comp-tabs-primary-selected-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-primary-selected-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-primary-selected-label-emphasis | — | 0 |
--tanrei-comp-tabs-primary-selected-state-layer-color | comp.tabs.primary.selected.covered-label.color | |
--tanrei-comp-tabs-primary-selected-underline-color | — | |
--tanrei-comp-tabs-primary-state-layer-color | comp.tabs.primary.label.color | |
--tanrei-comp-tabs-primary-track-color | sys.color.secondary-container | |
--tanrei-comp-tabs-primary-underline-color | — | |
--tanrei-comp-tabs-primary-underline-shape | sys.shape.corner.extra-small | 3px |
--tanrei-comp-tabs-primary-underline-thickness | — | 3px |
--tanrei-comp-tabs-secondary-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-active-indicator-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-tabs-secondary-active-indicator-thickness | sys.border.thick | 2px |
--tanrei-comp-tabs-secondary-container-color | — | |
--tanrei-comp-tabs-secondary-covered-icon-color | sys.color.on-secondary | |
--tanrei-comp-tabs-secondary-covered-label-color | sys.color.on-secondary | |
--tanrei-comp-tabs-secondary-disabled-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-disabled-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-secondary-disabled-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-secondary-disabled-container-color | — | |
--tanrei-comp-tabs-secondary-disabled-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-disabled-covered-label-color | — | |
--tanrei-comp-tabs-secondary-disabled-icon-color | — | |
--tanrei-comp-tabs-secondary-disabled-label-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-container-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-covered-label-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-icon-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-label-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-state-layer-color | — | |
--tanrei-comp-tabs-secondary-disabled-selected-underline-color | — | |
--tanrei-comp-tabs-secondary-disabled-state-layer-color | — | |
--tanrei-comp-tabs-secondary-disabled-track-color | — | |
--tanrei-comp-tabs-secondary-disabled-underline-color | — | |
--tanrei-comp-tabs-secondary-disabled-underline-shape | — | 0px |
--tanrei-comp-tabs-secondary-disabled-underline-thickness | — | 2px |
--tanrei-comp-tabs-secondary-focused-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-focused-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-secondary-focused-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-secondary-focused-container-color | — | |
--tanrei-comp-tabs-secondary-focused-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-focused-covered-label-color | — | |
--tanrei-comp-tabs-secondary-focused-icon-color | — | |
--tanrei-comp-tabs-secondary-focused-label-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-container-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-covered-label-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-icon-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-label-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-state-layer-color | — | |
--tanrei-comp-tabs-secondary-focused-selected-underline-color | — | |
--tanrei-comp-tabs-secondary-focused-state-layer-color | — | |
--tanrei-comp-tabs-secondary-focused-track-color | — | |
--tanrei-comp-tabs-secondary-focused-underline-color | — | |
--tanrei-comp-tabs-secondary-focused-underline-shape | — | 0px |
--tanrei-comp-tabs-secondary-focused-underline-thickness | — | 2px |
--tanrei-comp-tabs-secondary-hovered-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-hovered-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-secondary-hovered-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-secondary-hovered-container-color | — | |
--tanrei-comp-tabs-secondary-hovered-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-hovered-covered-label-color | — | |
--tanrei-comp-tabs-secondary-hovered-icon-color | — | |
--tanrei-comp-tabs-secondary-hovered-label-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-container-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-covered-label-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-icon-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-label-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-state-layer-color | — | |
--tanrei-comp-tabs-secondary-hovered-selected-underline-color | — | |
--tanrei-comp-tabs-secondary-hovered-state-layer-color | — | |
--tanrei-comp-tabs-secondary-hovered-track-color | — | |
--tanrei-comp-tabs-secondary-hovered-underline-color | — | |
--tanrei-comp-tabs-secondary-hovered-underline-shape | — | 0px |
--tanrei-comp-tabs-secondary-hovered-underline-thickness | — | 2px |
--tanrei-comp-tabs-secondary-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-secondary-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-secondary-pressed-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-pressed-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-secondary-pressed-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-secondary-pressed-container-color | — | |
--tanrei-comp-tabs-secondary-pressed-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-pressed-covered-label-color | — | |
--tanrei-comp-tabs-secondary-pressed-icon-color | — | |
--tanrei-comp-tabs-secondary-pressed-label-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-active-indicator-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-container-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-covered-icon-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-covered-label-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-icon-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-label-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-state-layer-color | — | |
--tanrei-comp-tabs-secondary-pressed-selected-underline-color | — | |
--tanrei-comp-tabs-secondary-pressed-state-layer-color | — | |
--tanrei-comp-tabs-secondary-pressed-track-color | — | |
--tanrei-comp-tabs-secondary-pressed-underline-color | — | |
--tanrei-comp-tabs-secondary-pressed-underline-shape | — | 0px |
--tanrei-comp-tabs-secondary-pressed-underline-thickness | — | 2px |
--tanrei-comp-tabs-secondary-selected-active-indicator-color | sys.color.secondary | |
--tanrei-comp-tabs-secondary-selected-container-color | — | |
--tanrei-comp-tabs-secondary-selected-covered-icon-color | sys.color.on-secondary | |
--tanrei-comp-tabs-secondary-selected-covered-label-color | sys.color.on-secondary | |
--tanrei-comp-tabs-secondary-selected-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-secondary-selected-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-secondary-selected-label-emphasis | — | 0 |
--tanrei-comp-tabs-secondary-selected-state-layer-color | comp.tabs.secondary.selected.covered-label.color | |
--tanrei-comp-tabs-secondary-selected-underline-color | — | |
--tanrei-comp-tabs-secondary-state-layer-color | comp.tabs.secondary.label.color | |
--tanrei-comp-tabs-secondary-track-color | sys.color.secondary-container | |
--tanrei-comp-tabs-secondary-underline-color | — | |
--tanrei-comp-tabs-secondary-underline-shape | sys.shape.corner.none | 0px |
--tanrei-comp-tabs-secondary-underline-thickness | sys.border.thick | 2px |
--tanrei-comp-tabs-tonal-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-active-indicator-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-tabs-tonal-active-indicator-thickness | sys.border.thick | 2px |
--tanrei-comp-tabs-tonal-container-color | — | |
--tanrei-comp-tabs-tonal-covered-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-tonal-covered-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-tonal-disabled-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-disabled-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-tonal-disabled-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-tonal-disabled-container-color | — | |
--tanrei-comp-tabs-tonal-disabled-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-disabled-covered-label-color | — | |
--tanrei-comp-tabs-tonal-disabled-icon-color | — | |
--tanrei-comp-tabs-tonal-disabled-label-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-container-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-covered-label-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-icon-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-label-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-state-layer-color | — | |
--tanrei-comp-tabs-tonal-disabled-selected-underline-color | — | |
--tanrei-comp-tabs-tonal-disabled-state-layer-color | — | |
--tanrei-comp-tabs-tonal-disabled-track-color | — | |
--tanrei-comp-tabs-tonal-disabled-underline-color | — | |
--tanrei-comp-tabs-tonal-disabled-underline-shape | — | 0px |
--tanrei-comp-tabs-tonal-disabled-underline-thickness | — | 2px |
--tanrei-comp-tabs-tonal-focused-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-focused-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-tonal-focused-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-tonal-focused-container-color | — | |
--tanrei-comp-tabs-tonal-focused-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-focused-covered-label-color | — | |
--tanrei-comp-tabs-tonal-focused-icon-color | — | |
--tanrei-comp-tabs-tonal-focused-label-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-container-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-covered-label-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-icon-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-label-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-state-layer-color | — | |
--tanrei-comp-tabs-tonal-focused-selected-underline-color | — | |
--tanrei-comp-tabs-tonal-focused-state-layer-color | — | |
--tanrei-comp-tabs-tonal-focused-track-color | — | |
--tanrei-comp-tabs-tonal-focused-underline-color | — | |
--tanrei-comp-tabs-tonal-focused-underline-shape | — | 0px |
--tanrei-comp-tabs-tonal-focused-underline-thickness | — | 2px |
--tanrei-comp-tabs-tonal-hovered-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-hovered-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-tonal-hovered-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-tonal-hovered-container-color | — | |
--tanrei-comp-tabs-tonal-hovered-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-hovered-covered-label-color | — | |
--tanrei-comp-tabs-tonal-hovered-icon-color | — | |
--tanrei-comp-tabs-tonal-hovered-label-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-container-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-covered-label-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-icon-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-label-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-state-layer-color | — | |
--tanrei-comp-tabs-tonal-hovered-selected-underline-color | — | |
--tanrei-comp-tabs-tonal-hovered-state-layer-color | — | |
--tanrei-comp-tabs-tonal-hovered-track-color | — | |
--tanrei-comp-tabs-tonal-hovered-underline-color | — | |
--tanrei-comp-tabs-tonal-hovered-underline-shape | — | 0px |
--tanrei-comp-tabs-tonal-hovered-underline-thickness | — | 2px |
--tanrei-comp-tabs-tonal-icon-color | sys.color.on-surface-variant | |
--tanrei-comp-tabs-tonal-label-color | sys.color.on-surface-variant | |
--tanrei-comp-tabs-tonal-pressed-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-pressed-active-indicator-shape | — | 9999px |
--tanrei-comp-tabs-tonal-pressed-active-indicator-thickness | — | 2px |
--tanrei-comp-tabs-tonal-pressed-container-color | — | |
--tanrei-comp-tabs-tonal-pressed-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-pressed-covered-label-color | — | |
--tanrei-comp-tabs-tonal-pressed-icon-color | — | |
--tanrei-comp-tabs-tonal-pressed-label-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-active-indicator-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-container-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-covered-icon-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-covered-label-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-icon-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-label-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-state-layer-color | — | |
--tanrei-comp-tabs-tonal-pressed-selected-underline-color | — | |
--tanrei-comp-tabs-tonal-pressed-state-layer-color | — | |
--tanrei-comp-tabs-tonal-pressed-track-color | — | |
--tanrei-comp-tabs-tonal-pressed-underline-color | — | |
--tanrei-comp-tabs-tonal-pressed-underline-shape | — | 0px |
--tanrei-comp-tabs-tonal-pressed-underline-thickness | — | 2px |
--tanrei-comp-tabs-tonal-selected-active-indicator-color | sys.color.secondary-container | |
--tanrei-comp-tabs-tonal-selected-container-color | — | |
--tanrei-comp-tabs-tonal-selected-covered-icon-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-tonal-selected-covered-label-color | sys.color.on-secondary-container | |
--tanrei-comp-tabs-tonal-selected-icon-color | sys.color.on-surface-variant | |
--tanrei-comp-tabs-tonal-selected-label-color | sys.color.on-surface-variant | |
--tanrei-comp-tabs-tonal-selected-label-emphasis | sys.typescale.emphasis.label | 100 |
--tanrei-comp-tabs-tonal-selected-state-layer-color | comp.tabs.tonal.selected.covered-label.color | |
--tanrei-comp-tabs-tonal-selected-underline-color | — | |
--tanrei-comp-tabs-tonal-state-layer-color | comp.tabs.tonal.label.color | |
--tanrei-comp-tabs-tonal-track-color | sys.color.secondary-container | |
--tanrei-comp-tabs-tonal-underline-color | — | |
--tanrei-comp-tabs-tonal-underline-shape | sys.shape.corner.none | 0px |
--tanrei-comp-tabs-tonal-underline-thickness | sys.border.thick | 2px |
--tanrei-comp-tabs-track-padding | sys.space.space50 | 4px |
--tanrei-comp-tabs-track-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-tabs-track-visible | — | 1 |
--tanrei-comp-tabs-label-typescale | sys.typescale.label | 500 14px/1.4286 |
测试样例
作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。
variant: primary (36)
primary
variant: primary value: overview label: Project items: 3 primary-fill
variant: primary fullWidth: true value: overview label: Project items: 3 primary-fill-truncated
variant: primary fullWidth: true value: notifications label: Account items: 3 primary-icons
variant: primary value: overview items: 3 primary-icon-only
variant: primary value: overview items: 3 primary-icon-only-fill
variant: primary fullWidth: true value: overview items: 3 primary-single-characters
variant: primary value: m label: Size items: 4 primary-second-selected
variant: primary value: activity items: 3 primary-scrollable
variant: primary value: overview items: 6 primary-disabled
variant: primary value: overview items: 3 primary-selected-disabled
variant: primary value: billing items: 3 primary-two-icons
variant: primary value: light label: Theme items: 2 primary-two-icons-second-selected
variant: primary value: dark label: Theme items: 2 shared-choice
variant: primary value: flutter label: Platform group: fixture-platform items: 4 shared-choice-lacking
variant: primary value: react label: Platform group: fixture-platform items: 2 primary-no-track
variant: primary track: false value: overview label: Project items: 3 primary-small
variant: primary size: small value: overview label: Project items: 3 primary-small-no-track
variant: primary size: small track: false value: overview label: Project items: 3 primary-small-fill
variant: primary size: small fullWidth: true value: overview label: Project items: 3 primary-small-icons
variant: primary size: small value: overview items: 3 primary-no-track-icon-only
variant: primary track: false value: overview items: 3 primary-small-icon-only
variant: primary size: small value: overview items: 3 primary-small-single-characters
variant: primary size: small value: m label: Size items: 4 primary-no-track-disabled
variant: primary track: false value: overview items: 3 primary-small-disabled
variant: primary size: small value: overview items: 3 primary-long-labels
variant: primary value: notifications label: Account items: 5 primary-small-long-labels
variant: primary size: small value: notifications label: Account items: 5 primary-rtl
variant: primary value: overview label: Project direction: rtl items: 3 primary-small-rtl
variant: primary size: small value: overview label: Project direction: rtl items: 3 primary-no-transition
variant: primary value: overview transition: none label: Project items: 3 primary-slide
variant: primary value: overview transition: slide label: Project items: 3 vertical
variant: primary orientation: vertical value: general label: Settings items: 5 vertical-small
variant: primary size: small orientation: vertical value: general label: Settings items: 5 vertical-scroll
variant: primary orientation: vertical value: november label: Month items: 12 vertical-rtl
variant: primary orientation: vertical value: notifications label: Settings direction: rtl items: 5 vertical-disabled-tab
variant: primary orientation: vertical value: general label: Settings items: 5 variant: secondary (14)
secondary
variant: secondary value: overview label: Project items: 3 secondary-fill
variant: secondary fullWidth: true value: overview label: Project items: 3 secondary-icons
variant: secondary value: overview items: 3 secondary-icons-fill
variant: secondary fullWidth: true value: overview items: 3 secondary-scrollable
variant: secondary value: analytics items: 6 secondary-disabled
variant: secondary value: overview items: 3 secondary-two-icons
variant: secondary value: light label: Theme items: 2 secondary-no-track
variant: secondary track: false value: overview label: Project items: 3 secondary-small
variant: secondary size: small value: overview label: Project items: 3 secondary-small-no-track
variant: secondary size: small track: false value: overview label: Project items: 3 secondary-small-icons
variant: secondary size: small value: overview items: 3 secondary-no-track-selected-disabled
variant: secondary track: false value: billing items: 3 secondary-rtl
variant: secondary value: overview label: Project direction: rtl items: 3 vertical-secondary
variant: secondary orientation: vertical value: notifications label: Settings items: 5 variant: tonal (34)
tonal
variant: tonal value: overview label: Project items: 3 tonal-no-track
variant: tonal track: false value: overview label: Project items: 3 tonal-small
variant: tonal size: small value: overview label: Project items: 3 tonal-small-no-track
variant: tonal size: small track: false value: overview label: Project items: 3 tonal-track-on
variant: tonal track: true value: overview label: Project items: 3 tonal-fill
variant: tonal fullWidth: true value: overview label: Project items: 3 tonal-no-track-fill
variant: tonal track: false fullWidth: true value: overview label: Project items: 3 tonal-small-no-track-fill
variant: tonal size: small track: false fullWidth: true value: standard label: Contrast items: 3 tonal-small-no-track-fill-truncated
variant: tonal size: small track: false fullWidth: true value: notifications label: Account items: 3 tonal-second-selected
variant: tonal value: activity label: Project items: 3 tonal-no-track-second-selected
variant: tonal track: false value: activity label: Project items: 3 tonal-icons
variant: tonal value: board items: 3 tonal-no-track-icons
variant: tonal track: false value: board items: 3 tonal-small-no-track-icons
variant: tonal size: small track: false value: overview items: 3 tonal-icon-only
variant: tonal value: overview items: 3 tonal-no-track-icon-only
variant: tonal track: false value: overview items: 3 tonal-small-no-track-icon-only
variant: tonal size: small track: false value: overview items: 3 tonal-disabled
variant: tonal value: overview items: 3 tonal-selected-disabled
variant: tonal value: billing items: 3 tonal-no-track-disabled
variant: tonal track: false value: overview items: 3 tonal-no-track-selected-disabled
variant: tonal track: false value: billing items: 3 tonal-small-no-track-disabled
variant: tonal size: small track: false value: overview items: 3 tonal-long-labels
variant: tonal value: notifications label: Account items: 5 tonal-no-track-long-labels
variant: tonal track: false value: notifications label: Account items: 5 tonal-small-no-track-long-labels
variant: tonal size: small track: false value: notifications label: Account items: 5 tonal-rtl
variant: tonal value: overview label: Project direction: rtl items: 3 tonal-no-track-rtl
variant: tonal track: false value: overview label: Project direction: rtl items: 3 tonal-small-no-track-rtl
variant: tonal size: small track: false value: standard label: التباين direction: rtl items: 3 tonal-small-no-track-icons-rtl
variant: tonal size: small track: false value: activity direction: rtl items: 3 tonal-small-no-track-long-labels-rtl
variant: tonal size: small track: false value: notifications label: Account direction: rtl items: 5 tonal-no-transition
variant: tonal value: overview transition: none label: Project items: 3 tonal-small-no-track-no-transition
variant: tonal size: small track: false value: standard transition: none label: Contrast items: 3 tonal-no-track-no-transition
variant: tonal track: false value: web transition: none label: Platform items: 5 vertical-no-track
variant: tonal track: false orientation: vertical value: notifications label: Settings items: 5 生成自 specs/tabs.spec.json, tokens/comp/tabs.tokens.json, design/tokens/
用标签页把一个主题拆成多个视图,每次显示其中一个:项目的“概览”“动态”和“设置”,个人主页的“帖子”和“回复”,文件的“预览”和“历史记录”。这些视图是并列的,用户可以按任意顺序在它们之间切换。
要在应用的各个目的地之间移动,请使用导航栏或导航抽屉,因为那些是不同的主题。不要把标签页用于必须按顺序完成的步骤,也不要用它来筛选一个列表:筛选请用纸片(Chip)。标签页只切换视图,从不触发操作。
Tanrei 系统不提供分段按钮,因此当“几项里选一项”会改变下方显示的内容时,标签页也承担它的职责:日历上方的“日”“周”和“月”,或者一个页面的各个示例上方的“标准”“中”和“高”。这样的一行不需要面板。给它一个名称,说明选择的是什么。如果它应该在它所改变的内容旁边保持低调,就使用柔和变体,不带轨道;放在页眉里时,再用小号。设置一个随表单发送的值的选择,或者不改变任何用户看得见的东西的选择,不属于标签页。这类选择请用单选按钮。
| 组件 | 它是什么 | 用于 |
|---|---|---|
| 标签页 | 同一主题的多个视图,或者一个会改变下方显示内容的选择。其中一个标签页处于选中状态,方向键在移动的同时完成选中。 | 切换下方显示的内容:“概览”“动态”“设置”;“日”“周”“月”。 |
| 分段按钮 | 两到五个连在一起的选项,用来设置一个值,可以选一个或多个。默认的 Tanrei 系统不提供它;Material 预设和 cupertino 预设提供。 | 只有几个固定答案的设置,并且产品使用的是上述预设之一。 |
| 按钮组 | 排在一起的几个独立按钮。每个按钮单独按下,按钮组不保存任何选择。 | 相关的操作:“取消”和“保存”;“粗体”“斜体”和“下划线”。 |
一个简单的判断办法:如果选择会改变下方显示的内容,就是标签页,有没有面板都一样。如果它设置一个随表单发送的值,就是单选按钮;在提供分段按钮的系统里,也可以是分段按钮。如果按下去是执行某件事,就是按钮组。
| 选择 | 它做什么 | 用于 |
|---|---|---|
| 主要(primary) | 图标在中号时叠放在标签上方,在小号时排在标签之前,由设计系统按尺寸设定(comp.tabs.icon.placement)。指示器以主要变体的颜色绘制。 |
一个屏幕的主要视图,位于其内容的顶部。 |
| 次要(secondary) | 图标排在标签之前,处于同一行。指示器以次要变体的颜色绘制。 | 上述某个视图之内的视图,位于一行主要标签页的下方,或位于面板之内。 |
| 柔和(tonal) | 指示器是柔和的一层,选中的标签比其他标签颜色更深、字重更粗。图标排在标签之前。 | 不应把视线从它所改变的内容上引开的选择:代码示例的语言、对比度、单位。 |
中号(size,默认) |
这一行高 48,指示器高 40。 | 位于屏幕内容顶部的一行。 |
小号(size) |
这一行高 32,指示器高 24。标签两侧的内边距和图标都更小,并且在每种变体中,图标都排在标签之前。标签的文字大小不变。 | 页眉或工具栏里、位于小号按钮旁边的一行。 |
贴合内容(fullWidth 为 false,默认) |
每个标签页的尺寸取决于它的内容,每个标签都完整地保持在一行里,不管它多长、字号多大。轨道在最后一个标签页处结束;标签页放不下时,在轨道内横向滚动。指示器跟随选中标签页的内容。 | 大多数的行:标签页较多、标签长短不一、标签经过翻译或由用户填写。 |
填满(fullWidth 为 true) |
把这一行的宽度等分。标签在自己那一份里放不下时,在末尾以省略号截断。指示器与标签页的那一份同宽。 | 两到五个必须横跨整行的短标签。 |
fullWidth 在 Web 上写作 full-width,在 SwiftUI、Compose 和 React Native 上写作 fullWidth;在 Flutter 上则写作它的反义 isScrollable,默认为 true,与 Flutter 自己的 TabBar 一样。
在两种布局中,标签都不会折到第二行,也不会在词中间断开;图标也不会为了腾出空间而缩小。在填满的一行里,被截断的标签仍然是该标签页完整的无障碍名称,平台会在能做到的地方把它完整显示出来:在 Web 上是悬停和键盘聚焦时的普通工具提示,在 Compose 上是长按和悬停时的工具提示,在 Flutter 上是 Tooltip,在 SwiftUI 上是大内容查看器(在 macOS 上还有 .help)。React Native 没有自己的工具提示,所以那里不会完整显示。
三种变体是同一个控件。它们的区别只在于选中状态的视觉分量,别无其他:按键、角色和告知辅助技术的内容都相同。
设计系统可以添加变体和尺寸。它添加的变体按主要标签页的方式布局。
小号的一行与小号的按钮等高,所以两者可以在页眉里共处一行。它比最小目标矮,也不会为此而加高。在这一行的正上方或正下方、与某个标签页对齐的位置按下,仍然会选中那个标签页。位于那里的控件保留它自己的按下:按在这一行下方的按钮上,操作的是那个按钮。文本放大时,小号的一行像中号的一行那样增高,不会有任何内容被截断。
标签页与开关共用一个模型:轨道上的一个滑块,它会滑向被选中的位置,也可以用手把它滑过去。开关的两个状态并不对等,所以它的整体外观会在两者之间改变。标签页是对等的选项,所以它的轨道从不改变,只有指示器在移动。
- 轨道。 整行背后的一块柔和的填充,两端是完全的圆角,没有轮廓。它与这一行等高。在贴合内容的一行里,它在最后一个标签页之后留出一小段内缩就结束;在填满的一行里,它与这一行同宽。无论选中哪个标签页、标签页处于什么状态,它都是一样的。标签页放不下时,轨道保持不动,标签页在轨道内滚动,并按轨道的圆角两端裁切。
- 指示器。 选中标签页的内容背后是指示器。它的高度等于这一行的高度减去轨道上下的内缩,所以它在每个标签页上高度相同:在默认系统中,48 高的行里是 40;主要标签页把图标叠在标签上方时是 56。在贴合内容的一行里,它的宽度等于标签页的内容加上内边距;如果内容的宽度不超过高度,它就是一个圆:只有一个图标、只有一个字母的标签、一个全角字符。在填满的一行里,它与标签页的那一份同宽。这条规则对主要和次要标签页都一样。
- 三边的内缩相同。 在任何行高下,指示器与轨道顶部、与轨道底部,以及(在第一个和最后一个标签页上)与轨道端部之间,都保持同一个内缩。它的圆角端与轨道的圆角端同心。指示器为圆形的标签页与这个圆同宽;最小目标更大时,与最小目标同宽。
- 悬停、聚焦和按下画在指示器在该标签页上占据的那个框里(标签页未选中时,则是它选中后会占据的那个框),从不画在标签页的整个矩形上。在选中的标签页上,它们画在指示器之上,颜色取指示器所覆盖内容的颜色,所以选中的标签页也像其他标签页一样响应悬停和按下。整个标签页仍然会响应指针。
- 不带轨道的一行。 把
track设为 false,这一行背后的填充就不绘制。其他一切都不变:这一行保持原来的尺寸、内缩、每个标签页的位置和每一种颜色,所以带轨道的一行和不带轨道的一行可以对齐。这一行所处的地方本身已经是一个栏时,就这样用:页眉、卡片、代码示例的标题。不设置时,track跟随设计系统(comp.tabs.track.visible,默认系统中是开启的)。在 Web 上,这个属性(attribute)写作track="false"。 - 柔和变体的指示器。 它是一层半透明的色层,取选中标签的颜色,叠在轨道上,或者叠在这一行所处的任何东西上,所以在每一种表面上,它与底色之间都相差同样的一级。它有意做成一个低调的标记,所以选中的标签是第二个标记:颜色更深,字重也粗一级,因此选中状态从不只靠颜色来表达。每个标签页都按较粗字重的标签预留空间,所以选中项改变时,这一行里没有任何东西移动。
- 被覆盖的标签。 标签和图标只在被指示器盖住的地方,才以“被覆盖”的颜色绘制。指示器滑动或被拖动时,它经过的每个标签只在被盖住的部分发生变化;指示器落定的那一刻,新标签页的标签已被完全覆盖。被覆盖的那一份用系统的颜色绘制,从不用混色,所以它的对比度就是令牌所保证的对比度。
设计系统还可以在这一行的底边、选中标签页的下方画一条线,并沿整行画一条分隔线。这两者在每个系统里都存在,在默认系统中是透明的。Material 预设为它们着色,并把轨道和指示器设为透明,就像 Material 的标签页那样。
- 轻点仍然会选中。 而用触摸、触控笔或鼠标在这一行上拖动,则会让指示器随指针移动。它的位置、宽度,以及它下方标签的被覆盖颜色都连续变化,没有阈值。
- 松手时做出选择。 选中的是指示器离得更近的那个标签页,或者快速滑动所朝向的那个。如果那是另一个标签页,这一行就选中它,并触发一次
change;指示器途中经过的标签页不会触发事件。如果那就是原本选中的标签页,指示器会回到原位,不触发任何事件。 - 途中其他一切都不变。 在松手之前,选择、面板、键盘焦点以及告知辅助技术的内容都保持原样。按 Escape,或平台取消了这次指针操作,都会让指示器回到选中的标签页。
- 滚动照常工作。 先向上或向下的拖动会滚动页面。在 Web 上,在标签页可滚动的一行里,只有从选中的标签页开始的拖动才会移动指示器;其他任何横向拖动都是滚动标签页。
- 减弱动态效果。 指示器仍然跟随指针,因为那是用户自己的手指在移动;落到标签页上时则立即到位。
transition 说明这一行怎样绘制新的选择。
| 值 | 它做什么 |
|---|---|
slide |
指示器滑向新的标签页并随之改变大小,标签在它之下改变颜色。不在视野内的标签页会以一段动效滚动到视野内。 |
none |
新的选择在一帧之内完整绘制。没有任何东西位移,也没有任何东西淡入淡出。 |
auto(默认) |
采用设计系统的答案(comp.tabs.motion.transition,默认系统中是 slide)。 |
这一行位于它刚刚改变的内容旁边、滑动的指示器会成为唯一还在动的东西时,请使用 none。在 none 之下,在这一行上拖动仍然会带着指示器移动,松手时指示器立即落到它的标签页上。无论取哪个值,悬停、聚焦和按下的响应都相同。在减弱动态效果时,无论 transition 怎么设置,选择都按 none 的方式绘制。
标签页视图为它的面板另有一个自己的 transition。把它设为 none,视图绘制的那一行标签页也不再滑动。
- 按值。 每个标签页都有一个
value,在它所在的行内唯一。增加、移除标签页或调整顺序之后,值依然有效;换成索引的话,就会悄悄选中另一个视图。value不对应任何标签页时,不选中任何标签页,也不显示任何面板,调试构建会报告这一点。 - 自动激活。 用方向键移动焦点的同时也会选中标签页,面板随即切换。如果面板需要时间加载,请在面板内显示加载状态,而不是推迟切换。
- 禁用的标签页会被跳过。 方向键会越过它们,它们也永远不是 Tab 停靠点,因为落在上面就等于选中了一个不能选中的标签页。这一点与列表和菜单不同:那里禁用的项仍然可以到达,因为移到上面并不会选中任何东西。
- 指示器会滑动。 选择改变时,指示器从旧标签页滑到新标签页并随之改变大小,途中可能由圆形拉宽成胶囊形。在减弱动态效果时,它立即到位。这一行首次出现时,或者选中的标签页在没有新选择的情况下改变了大小(字体加载完成、文本大小改变),指示器直接就位,不滑动。标签页被选中或获得焦点时,如果它有一部分滚出了视野,就会被滚动到视野内。
- 这一行绘制自己的轨道。 把它放在屏幕或应用栏的表面上即可。它不需要自己的容器。
- 在标签页视图中。 自己绘制一行标签页的标签页视图用的就是这些标签页,令牌和行为都相同。用户轻扫标签页视图时,视图通过
indicatorOffset来移动指示器:在选中的标签页上为 0,在下一个可用的标签页上为 1,在上一个可用的标签页上为 −1,手指移动时取其间的值。指示器逐帧跟随它,自己不做滑动。除非你要自己驱动指示器,否则让它保持为 0。在视图上轻扫与在它那一行上拖动,是同一个手势发生在两个地方:轻扫移动一个面板,面板随之移动;在这一行上拖动可以越过多个标签页,面板则等到松手才切换。
当同一个问题在许多地方都要回答时——本站每个代码示例的平台、一份报告中每个数字的单位——就给这些行指定同一个 group。在其中一行选中某个标签页,其他每一行里值相同的标签页也会被选中;没有这个标签页的行则保持自己的选择。
- 按值对应。 组跟随的是标签页的
value,从不是它的标签,所以标签经过翻译或改写之后,选择仍然保留。 - 一行移动,一行触发事件。 用户操作的那一行会滑动,并触发一次
change。其他每一行立即采用这个选择,不带动效,不夺取焦点,不滚动,也不触发任何事件。即使它上方的行改变了高度,用户操作的那一行在窗口中的位置也保持不变。 - 记住选择是另一回事。
rememberAs指定一个键,用户的选择以这个键保存在其设备上,并在这一行首次绘制之前恢复。没有键就什么都不保存。键由你决定,并原样使用;保存的只有标签页的值。在你的产品所面向的地区,记住偏好是否需要征得同意,由你自行判断。 - 在 Web 上,由标签页来维护组。
group和remember-as是 HTML 属性,键是localStorage的一个键。在 SwiftUI、Compose、Flutter 和 React Native 上,传入同一个选择状态的各行本来就共享这个状态,由你的产品来保存和存储它。
同一个组的两行:在任意一行里选一个标签页
马拉松全程 42.195 km。
马拉松全程 26.2 英里。
快走的速度是 6 km/h。
快走的速度是每小时 3.7 英里。
快走的速度是 3.2 节。
在这个演示中,“航海”只存在于第二行:选中它时,第一行保持自己的选择;一旦选择变回第一行也有的值,第一行又会跟着变化。两行都没有指定键,所以什么都不保存。
本站的标签页就是一个例子:每一组平台示例都是同属一个组的标签页视图。
把 orientation 设为 vertical,标签页会在起始边排成一列,放在它们所切换的视图旁边:设置页或对话框的分类、一条记录的各个部分。其他方面都是同一个组件:一个选中的标签页,一个会滑动、可拖动的指示器,面板,以及共享的选择。
放在面板旁的垂直标签页
语言、地区和起始页。
邮件、推送通知和每周摘要。
谁可以查看你的个人资料。
- 这一列。 在每种变体、每种尺寸下,每个标签页都和最宽的那个一样宽,图标在标签之前,位于起始边。标签页上下彼此相接,所以每个至少有最小目标那么高:小尺寸一列中的标签页高 44(触摸输入时为 48),而不是 32。要让这一列更宽,请使用
::part(tablist)。如果这一列比最宽的标签页还窄,标签会被截断,并在工具提示中完整显示。 - 指示器与轨道。 指示器是标签页四边各减去轨道内边距后的大小。轨道的圆角等于指示器的圆角加上这段内边距,因此无论这一列多高,两者在两端都保持同心。在选中标签页下方画线的设计系统,会把这条线画在这一列的末端边上、靠近面板的一侧,分隔线也沿这条边延伸。
- 按键。 下箭头和上箭头移到下一个和上一个标签页并选中它,Home 和 End 移到两端。右箭头和左箭头不起作用,因此仍归页面使用。辅助技术会被告知这个标签列表是垂直的。
- 滚动。 给这一行设定高度后,放不下的标签页会在轨道内上下滚动。触摸用于滚动;只有鼠标或手写笔可以拖动指示器。
fullWidth不适用于一列。
- 一行里的标签页要么都带图标,要么都不带。图标在未选中时以轮廓样式绘制,选中时以填充样式绘制。
labelHidden只绘制图标。标签仍然必须提供:它仍是该标签页的无障碍名称。设置了labelHidden却没有图标的标签页照样会绘制标签,调试构建会报告这一点。- 标签为空且没有图标的标签页没有内容:它的指示器是一个空的圆,标签页也没有无障碍名称。调试构建会报告空标签;请给这个标签页提供标签。
- 要在标签页上显示计数,请自己在它的图标上放一个 Tanrei 徽标。标签页没有徽标属性。
- 标签要简短:用一两个词为视图命名,例如“概览”或“账单”。在填满的一行里,长标签会被截断。
- 标签较长、经过翻译、由用户填写,或会以大号文本阅读时,保持默认的贴合内容。
- 保持标签页的顺序稳定,让用户能在原来的位置找到视图。
- 把次要标签页放在主要标签页的某个视图里,而不是反过来。
- 有时不可用的标签页要留在原位并禁用,这样这一行的形状不会改变。
- 同一主题的视图很多或标签较长时,使用垂直标签页:设置页的分类、一条记录的各个部分。
- 不要把标签页用于应用导航。那是导航栏或导航抽屉的职责。
- 不要让标签页打开对话框、启动操作或离开当前屏幕。标签页没有自己的目标地址:要在页面之间移动,请使用链接、导航栏或导航抽屉。
- 不要嵌套两行同一变体的标签页。
- 不要在同一行里混用带图标和不带图标的标签页。
- 不要用垂直标签页来选择在应用中去哪里。由目的地组成的一列是导航抽屉的职责:它的项目是链接,选择其中一个会改变地址。
- 角色
tablist (radiogroup while the row has no panel)- 标注
- Each tab is named by its label, drawn or not (labelHidden), and whole even when it is drawn truncated. The row is named by its own label, which in a row without panels says what is chosen. The selected tab's panel is named by its tab; a row with no panel names nothing but itself and its tabs. Icons, the covered copies of labels and icons, the track, the indicator, the underline and the divider are decorative.
- 最小目标
- 即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px
键盘操作
| 按键 | 动作 |
|---|---|
| Tab / Shift+Tab | Moves focus to the selected tab, and from it into its panel or, where the row has no panel, to whatever follows the row. |
| Right Arrow / Left Arrow | Horizontal, or any row without panels: moves focus to the next or previous enabled tab, wrapping, and selects it (mirrored in right-to-left layouts). Vertical with panels: does nothing. |
| Down Arrow / Up Arrow | Vertical, or any row without panels: moves focus to the next or previous enabled tab, wrapping, and selects it. Horizontal with panels: does nothing (the page scrolls). |
| Home / End | Moves focus to the first or last enabled tab and selects it. |
| Enter / Space | Selects the focused tab (needed only after the product has moved focus without selecting). |
| Escape (during a drag) | Cancels a drag that a pointer has in progress; the indicator returns to the selected tab and nothing is selected. |
备注
- Activation is automatic: moving focus with the arrow keys selects the tab, as the WAI-ARIA tabs pattern recommends when a panel shows without a noticeable delay. A product whose panels load slowly shows a loading state in the panel; a manual mode (focus first, Enter to select) can be added later as a new prop without breaking this spec.
- Tabs and the switch share one model: a thumb on a track that slides to what is chosen and follows a finger. A switch has two states, and its track, outline and handle differ between them; tabs have two or more equal choices, so their track never changes and the active indicator alone moves.
- The drag is never the only way (WCAG 2.5.7): every tab is selected by a single tap or click on it, and by the arrow keys. With a screen reader running, a one-finger drag belongs to the reader; tabs are then selected by the reader's activation, as before.
- A drag changes what is drawn, continuously (the indicator's place and width, and the labels' colors under it), and nothing that assistive technology is told: no tab is exposed as selected on the way, no panel changes, and no change is emitted for the tabs the indicator passes (TAB-30). There is no threshold in the look; the place where two tabs meet decides only what a release chooses.
- Selection is the row's: value holds the selected tab's value, and change carries the new one. A tab's disabled is its own.
- Web: `tanrei-tab-panel` elements are matched to tabs by their `value` attribute, whether they are the row's children or a linked `tanrei-tab-view`'s, and get role tabpanel, aria-labelledby and the tab's aria-controls; a row with none is a radio group of radios (TAB-42). SwiftUI, Compose, Flutter and React Native have no tab panel role; there the selected panel follows the row in reading order, and each README says so.
- Tabs switch views of one subject. Moving between destinations of an app belongs to the navigation bar or drawer.
- A Tab view (`tab-view`) draws its own row of tabs from its panels by default, or is linked to a row placed elsewhere; either way it holds the panels. The link is by value: the Tab view shows the panel of `value`, follows the tabs when a tab is selected, and sets `value` on both and emits its own change when the user swipes it (TBV-2, TBV-4). While a swipe is in progress it sets `indicatorOffset`, which places the active indicator and the underline between two tabs (TAB-26); TAB-24 makes the covered colors follow the indicator wherever it is drawn. A drag across the row and a swipe of the view are one gesture in two places: the indicator follows the finger, the selection is made on release (from halfway, or by a fling), a cancelled gesture returns it, and nothing is announced before the release. They differ in reach: a swipe moves one panel, and the panels travel with it; a drag across the row can pass several tabs, and the panels wait for the release, then switch as for a selected tab (TBV-2). A row without a Tab view shows its own panels (TAB-13).
- Text size never hides a label: a tab that hugs its content grows wider and the row scrolls; a tab that shares the row's width truncates its label, which stays the tab's accessible name and is shown whole where the platform can (TAB-25). A product whose labels are long, translated, written by users, or read at large text sizes keeps the default hug.
- 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.
- A shared choice (group) is one decision shown in many places: a person who picks a platform, a language or a unit once should not be asked again by every sample on the page. The rows that follow change silently and at once (TAB-38): nothing is announced, focus stays on the tab the person used, and each row tells assistive technology its selected tab when the person reaches it. A change of content elsewhere on the page is not a change of context (WCAG 3.2.2), but it should not surprise: rows of one group carry the same labels for the same values (WCAG 3.2.4), and a product whose rows are far apart says near the first one that the choice applies to the whole page.
- Tabs of different rows are matched by value, never by label, so a choice survives a translated label, an icon-only tab and a change of wording, and a remembered choice still holds after the person changes language.
- Remembering (rememberAs) is off unless the product names a key, and what is kept is one short string the product itself wrote as a tab's value: no text a person typed, no identifier, nothing sent anywhere by the component. Whether keeping a preference on the device needs the person's consent where the product is used is the product's to decide, which is why there is no default key and no remembering by default. A person who blocks storage loses nothing but the memory (TAB-39).
- The page does not move under the person (TAB-40): a row whose choice changes the height of samples above it stays under the pointer and under keyboard focus, which is what WCAG 2.4.3 and 3.2.2 ask of a control that changes content elsewhere, and what keeps a person using magnification from losing their place.
- A row without panels (TAB-42) is still drawn as tabs when one choice changes what is shown below it: Day, Week and Month over a calendar; Standard, Medium and High over the examples of a page; a unit over a table; a filter over a list. It is announced as what it is, one choice among a few: a radio group named by the row's label, whose tabs are radio buttons, the selected one checked. A tab list promises panels: in the WAI-ARIA tabs pattern every tab controls the panel it shows, and a screen reader that says "tab" tells a person that a panel follows, so a tab list with no panel is announced wrongly. The radio group keeps what a person does the same: one Tab stop, and arrows that move and select, as a radio group's arrows check what they reach. The row knows which it is from what it holds — its own panels, a Tab view's, or the panel of a component that draws it — and not from a prop, so what it says can never disagree with what follows it: a row is a tab list from the moment it has a panel. The row's label tells a person what they are choosing, and the product draws the result after the row in reading order where it can, so that a person who has chosen finds it by reading on; where the choice reaches the whole page, the label or the words beside the row say so. A choice that sets a value sent with a form is radio buttons, which the form submits.
- Variant, track, size and transition say how the row looks and moves, never what it is: every combination is exposed alike, as a tab list where the row has a panel and as a radio group where it has none (TAB-42), with one keyboard model (TAB-49, TAB-50). Primary is the row of a screen's main views, secondary a row inside one of them, tonal the quiet row — in the heading of a sample, in a header, in a toolbar — where the choice should not draw the eye from what it changes.
- The selected tab is told to assistive technology as a state, and to sight by the indicator and by its label. A tonal row's indicator is a quiet mark on purpose: in the default system a layer about 1.4:1 from what it lies on, the same step on the track and on any surface, with the selected label darker than the others. It is not held to 3:1 against its ground, as the selected destination of a navigation drawer is not. A person who needs more has the system's medium and high contrast and the platform's forced colors, where the indicator is a filled highlight (TAB-45); a product that cannot accept the quiet mark uses secondary or primary, which are filled.
- Without its track (TAB-49) a row has no ground of its own: its labels are held on every surface role of the system, and a product that places a row on a color of its own checks the labels against it, as it does for text.
- A small row (TAB-44) is 32 tall and still pressed over the minimum target (TAB-5, WCAG 2.5.8): its hit area reaches above and below it, which a header or a toolbar has room for. Directly over content that answers a press at its top edge, that content wins the overlap; a product that stacks a small row tight against such content uses the medium size.
- transition none (TAB-43) takes away the motion the row plays by itself when the selection changes, in every motion setting; it takes away no way of selecting, and a drag across the row still carries the indicator under the finger. Where motion is reduced every row is drawn that way, and the state colors then change at once too (TAB-23), a ripple appearing whole (TAB-9).
- A vertical tab list (orientation vertical) is still tabs: one subject's views, the categories of a settings page or the sections of one record, with the arrow keys of its axis (Down and Up); a column without panels is a radio group, whose arrows move in both axes (TAB-42). It is not a menu of destinations — moving between the pages of an app belongs to the navigation drawer, whose items are links. A column never wraps a label: a column narrower than its widest tab truncates the label, which stays the tab's whole name, and shows it whole in a tooltip (TAB-61).
生成自 specs/tabs.spec.json, tokens/sys/measure.tokens.json
- 有面板的一行暴露为一个标签页列表,以它的
label命名。每个标签页都有标签页角色、选中状态、所在位置和标签页的总数,并以它的标签命名,无论标签是否绘制出来。 - 选中标签页的面板暴露为标签页面板,以它的标签页命名;在平台能够表达的地方,还会与该标签页关联。其他面板既不显示,也对辅助技术隐藏。面板里没有任何可聚焦的元素时,面板自身会变为可聚焦,让 Tab 键能够到达它。
- 这一行是一个 Tab 停靠点:选中的标签页。右方向键和左方向键移到下一个和上一个可用的标签页并选中它,到两端时循环。在从右到左的布局中,两者对调。Home 和 End 去往第一个和最后一个可用的标签页。Enter 和 Space 选中获得焦点的标签页。Tab 键从选中的标签页移入它的面板。
- 禁用的标签页会被方向键跳过,永远不是 Tab 停靠点,并被朗读为已禁用。
- 焦点环沿着指示器在获得焦点的标签页上占据的那个框绘制,形状与之相同,并且保持在轨道之内,所以可滚动的行不会裁切它。它只在键盘聚焦时显示。
- 标签页与相邻的标签页紧挨着,所以比最小目标窄的标签页会在宽度上变大到最小目标。比最小目标矮的一行(小号)不会加高:它的标签页会响应这一行上方和下方不远处的按下,并让位于那里已有的控件。
- 没有面板的一行是在几项里选一项,也就照此暴露:一个以它的
label命名的单选组,每个标签页是一个单选按钮,选中的那个处于已选中状态。如果暴露为标签页列表,屏幕阅读器用户会以为后面跟着一个面板。无论这一行是横向还是纵向,每个方向键都像在单选组里一样,移到下一个或上一个可用的标签页并选中它。这个选择在这一行下方改变了什么,要由你来绘制。Tab 键从选中的标签页移到这一行之后的内容上。一旦有了面板,这一行就成为标签页列表。 - 在强制颜色模式下,无论哪种变体、有没有轨道,指示器都是一块填充的高亮色,所以柔和变体的一行与主要变体的一行一样清楚地显示自己的选择。
- 文本放大绝不会让标签对辅助技术不可见。贴合内容的标签页会变宽,这一行则滚动。等分这一行的标签页会截断自己的标签,但标签仍是它的无障碍名称,并在平台能做到的地方完整显示。
- 图标、轨道、指示器、下划线和分隔线对辅助技术隐藏。指示器之下每个标签的“被覆盖”副本是装饰性的,所以每个标签只朗读一次。
- 在这一行上拖动绝不是唯一的方式:每个标签页都可以通过在它上面轻点或点击来选中,也可以用方向键选中。拖动期间,没有任何标签页暴露为选中,面板不变,也不朗读任何内容;松手时才完成选中,按 Escape 则取消拖动。屏幕阅读器运行时,单指拖动属于屏幕阅读器,标签页通过它的激活操作来选中。
- 在一个组里,跟随的行悄悄地变化。不朗读任何内容,焦点留在用户操作的那个标签页上,那一行在窗口中的位置也保持不变,所以使用放大功能的用户不会找不到原来的位置。用户到达每一行时,这一行会把自己选中的标签页告知辅助技术。
- 调试构建会报告:没有标签的标签页、为空或重复的值、不对应任何标签页的
value、隐藏了标签却没有图标的标签页、设计系统中不存在的变体或尺寸,以及标签页没有定义的transition。
已知的平台差距:
- SwiftUI、Compose 和 React Native 没有标签页面板角色。选中的面板按阅读顺序跟在这一行之后。
- SwiftUI、Compose、Flutter 和 React Native 仍然把没有面板的一行暴露为标签页,而不是单选组。
- SwiftUI 也没有标签页列表角色和标签页角色。这一行是一个以
label命名、带标签栏特征的容器,每个标签页是带选中特征的按钮。在 iOS 上,VoiceOver 根据标签栏特征说出标签页的位置;在其他系统上,位置是标签页的值(“2 of 3”),只有英文。 - SwiftUI 无法把面板与它的标签页关联起来,也无法判断面板里有没有可聚焦的东西,所以没有可聚焦内容的面板不会成为 Tab 停靠点。
- Compose 没有标签页列表角色。这一行是一个以
contentDescription命名的可选择的组,并把标签页的总数作为集合信息提供。每个标签页都有标签页角色、选中状态和所在位置。 - Compose 和 React Native 把面板交给你处理。它们不会隐藏其他面板,也不会让空面板可聚焦。
- Compose:放在标签页图标上的徽标不会被朗读,因为图标插槽是装饰性的。包住整个标签页的徽标会被朗读,但它画在标签页的角上。
- Compose:经由真实输入管线的方向键和滚动,以及 TalkBack 对每个标签页的角色和位置的朗读,目前只在 Robolectric 上测试过,尚未在真机上测试。
- Flutter 的语义中没有位置、没有标签页总数,也没有从标签页到其面板的关联。
TanreiTabBarView是一个标签页面板,以你传入的semanticLabels命名。 - React Native 没有标签页面板角色,也没有从标签页到其面板的关联。标签页把自己的位置作为值文本提供(“2 of 3”),除非你传入
positionText,否则是英文。 - React Native:方向键、把标签页滚动到视野内(及其从右到左时的偏移量)和滑动的指示器,尚未在真机上检查过。
- Web:用 Tab 键到达一个滚出视野的标签页时,浏览器会立即把它滚动到视野内,而不是按指示器的动效进行。
- Flutter:可滚动的行如果一开始选中的标签页不在视野内,会先在起始边缘显示一帧,然后才跳到该标签页。
- 为每个标签页提供标签,纯图标的标签页也不例外。
- 一个屏幕上有不止一行标签页时,用
label为这一行命名。写明这些视图是哪个主题的视图,例如“项目”。 - 让每个标签页的值在它所在的行内唯一,并且在屏幕显示期间保持稳定。
- 在 Compose 和 React Native 上,只渲染选中的面板,并紧接在这一行之后。面板里没有任何可聚焦的东西时,让面板可聚焦,并以它的标签页为它命名,让 Tab 键能够到达它。
- 在 Flutter 上,把每个标签页的标签通过
semanticLabels传给TanreiTabBarView。 - 在 React Native 上,用你的产品的语言传入
positionText。 - 面板需要时间加载时,在面板内显示加载状态。方向键在移动的同时就会选中。
- 同一个组里的各行,相同的值要用相同的标签。各行相距较远时,在第一行附近说明这个选择适用于整个页面。
rememberAs的键由你自己选定;如果你的产品需要隐私声明,请在其中写明它。没有键就什么都不保存。