メインコンテンツへスキップ
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
はじめにWebReactSwiftUIJetpack ComposeFlutterReact Nativeプリセット自分のデザインシステムを作るAI エージェントと Tanrei を使うブランドデザイントークンデザインシステムとカスタマイズTheme Studioカラータイポグラフィアイコン形状エレベーションモーションインタラクション状態振る舞いレイアウトアクセシビリティ言葉カラートークンタイプスケールトークン形状トークンエレベーショントークンモーショントークン状態トークンスペースと寸法のトークンコンポーネントボタンメインアクションボタンセグメントボタンボタングループメニュー付きボタンツールバーコピーボタンチェックボックスラジオボタンスイッチチップメニュースライダースライダーフィールドカレンダーDateTimePickerフィールドグループラジオグループチェックボックスグループセレクトセレクトフィールドメニューバーファイルアップロードファイルピッカー 上級者向けドロップゾーンテキスト入力欄 上級者向けテキストフィールドチップフィールドフィールドコンボボックス 上級者向けコンボボックスフィールド数値入力欄 上級者向け数値フィールドカラー入力欄 上級者向けカラーフィールドワンタイムコード入力欄 上級者向けワンタイムコードフィールド埋め込みテキスト入力欄 上級者向けチャットコンポーザーカード区切り線モーダルレイヤー 上級者向けダイアログリストリスト行ボトムシートサイドシートカルーセルコードブロックテーブルポップオーバーステップアコーディオン説明リスト図アプリシェルページヘッダーツリービュータイムライン画像プレビューハンドルサイズ変更できる枠スクロールエリアバッジプログレスインジケーターメーターナンバーティッカーローディングインジケータースナックバーツールチップキーボードショートカットタグインラインコードコールアウトアバター空の状態タググループスケルトンファイル項目色見本マーキーテキストタブタブビューアプリバーナビゲーションバーナビゲーションドロワー検索リンクページネーション前後のリンクステッパーパンくずリスト目次リンクカードFAB メニューステータスTanrei の作り方コントリビューションライセンスと帰属
ホームコンポーネントタブ

タブ

同じ対象の複数のビューを、トラックの上に置いたラベル付きのタブの並びで切り替えます。選択中のタブは 1 つで、タブの間を滑り、並びの上をドラッグする指にも付いてくるインジケーターがそれを示します。

content_copyMarkdown で表示
カテゴリ
ナビゲーション
トークン
comp.tabs
レベル
基礎
使用先
Tab view

概要

Web
tanrei-tabs
schedule
SwiftUI
TanreiTabs
schedule
Jetpack Compose
TanreiTabs
schedule
Flutter
TanreiTabBar
schedule
React Native
Tabs
schedule
提供しないバリアント
m3-expressive-like: tonal · m3-like: tonal

生成元 docs/status/status.json, specs/tabs.spec.json

仕様は改訂済み、各プラットフォームは追従待ち

このページは、現在の仕様のタブを説明しています。タブはトラックの上に並び、1 つのインジケーターが選択中のタブへ滑り、並びの上のドラッグにも付いてきます。タブは既定で中身に合わせた幅になり、ラベルは折り返さずに省略します。複数の並びで 1 つの選択を共有し、記憶することもできます(group と rememberAs)。scrollable は fullWidth に置き換わりました。並びには 3 つのバリアント(primary、secondary、tonal)と 2 つのサイズがあり、トラックを描かないことも、インジケーターが滑るかどうかを transition で指定することもできます。Web コンポーネントはこのすべてに対応しており、デモもそれを示しています。SwiftUI、Compose、Flutter、React Native は上の表で「追従待ち」です。追従するまでは以前の版(scrollable、Flutter では false が既定の isScrollable、線のインジケーター、折り返す固定幅のラベル。トラックもドラッグもありません)を提供しています。

プライマリーのタブと、選択中のタブのパネル

3 つのサービスがすべて稼働中です。

2 時間前にデプロイしました。

リージョン、ドメイン、アクセス権。

プレイグラウンド

コントロールごとに、実際のコンポーネントのプロパティを 1 つ設定します。コードも合わせて変わります。設定はページのアドレスに残るので、リンクを開くと同じ状態で表示されます。 コードは Web 用です。ほかのプラットフォームは「プラットフォーム別の名前対応」を参照してください。

使い方

タブの並びが持つ項目の種類は tab の 1 つだけです。タブの渡し方は、各プラットフォームで繰り返しの子要素を渡すときの流儀に従います。Web では子要素の <tanrei-tab>、SwiftUI ではビュービルダーの中で値のタグを付けた TanreiTab、Compose ではコンテンツラムダの中の TanreiTab の呼び出し、Flutter では tabs: に並べた TanreiTab ウィジェット、React Native ではプレーンなオブジェクトの配列 items です。

選択は、値によって並びが持ちます。value は選択中のタブの値で、change が新しい値を伝えます。SwiftUI では selection、Compose では selectedValue という名前です。空のままにすると、最初の有効なタブが選ばれます。タブの disabled はそのタブ自身のものです。

パネルを描くのはタブではありませんが、どのパネルを表示するかはタブが決めます。Web では、タブの後ろに置いた <tanrei-tab-panel> を value でタブと対応づけます。SwiftUI では各タブが自分のパネルを持ちます。Flutter では、TanreiTabBarView が選択中の値のパネルを表示します。Compose と React Native では、選択中のパネルを利用者側で並びのすぐ後ろに表示します。スワイプで切り替えるパネルには、代わりにタブビューを使います。タブビューは自分のタブの並びを描くか、アプリバーなどほかの場所に置いた並びとつながります。

<!-- 並びの選択中のタブは、<select> と同じく value です。 -->
<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 を指定した並びは 1 つの選択を示します。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';
  // 並びは change を発火する前に自分の value を更新します。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 を指定した並びは 1 つの選択を示します。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 のタグ付きタブと同じく、各タブが自分のパネルを持ち、選択中のものだけを表示します。
// バインディングへの書き込みが change にあたります。選択が 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 />}

仕様

セカンダリーのタブ、幅を等分する並び、中身に合わせてスクロールする並び、アイコンだけの並び

トーナルのタブ。トラックあり、トラックなし、トラックもトランジションもない小さい並び

構成要素

構成要素

Web パッケージが描くフィクスチャ 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
Web パッケージが描くフィクスチャ 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 — #000000 · 0%
--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 — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-primary-covered-icon-color sys.color.on-primary #ffffff
--tanrei-comp-tabs-primary-covered-label-color sys.color.on-primary #ffffff
--tanrei-comp-tabs-primary-disabled-active-indicator-color — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-primary-disabled-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-active-indicator-color — #1b1b1b · 12%
--tanrei-comp-tabs-primary-disabled-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-primary-disabled-selected-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-primary-disabled-track-color — #eeeeee
--tanrei-comp-tabs-primary-disabled-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-disabled-underline-shape — 3px
--tanrei-comp-tabs-primary-disabled-underline-thickness — 3px
--tanrei-comp-tabs-primary-focused-active-indicator-color — #000000 · 0%
--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 — #262626 · 10%
--tanrei-comp-tabs-primary-focused-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-focused-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-focused-icon-color — #262626
--tanrei-comp-tabs-primary-focused-label-color — #262626
--tanrei-comp-tabs-primary-focused-selected-active-indicator-color — #1b1b1b
--tanrei-comp-tabs-primary-focused-selected-container-color — #ffffff · 10%
--tanrei-comp-tabs-primary-focused-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-focused-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-focused-selected-icon-color — #262626
--tanrei-comp-tabs-primary-focused-selected-label-color — #262626
--tanrei-comp-tabs-primary-focused-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-primary-focused-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-focused-state-layer-color — #262626
--tanrei-comp-tabs-primary-focused-track-color — #eeeeee
--tanrei-comp-tabs-primary-focused-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-focused-underline-shape — 3px
--tanrei-comp-tabs-primary-focused-underline-thickness — 3px
--tanrei-comp-tabs-primary-hovered-active-indicator-color — #000000 · 0%
--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 — #262626 · 8%
--tanrei-comp-tabs-primary-hovered-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-hovered-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-hovered-icon-color — #262626
--tanrei-comp-tabs-primary-hovered-label-color — #262626
--tanrei-comp-tabs-primary-hovered-selected-active-indicator-color — #1b1b1b
--tanrei-comp-tabs-primary-hovered-selected-container-color — #ffffff · 8%
--tanrei-comp-tabs-primary-hovered-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-hovered-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-hovered-selected-icon-color — #262626
--tanrei-comp-tabs-primary-hovered-selected-label-color — #262626
--tanrei-comp-tabs-primary-hovered-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-primary-hovered-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-hovered-state-layer-color — #262626
--tanrei-comp-tabs-primary-hovered-track-color — #eeeeee
--tanrei-comp-tabs-primary-hovered-underline-color — #000000 · 0%
--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 #262626
--tanrei-comp-tabs-primary-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-primary-pressed-active-indicator-color — #000000 · 0%
--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 — #262626 · 10%
--tanrei-comp-tabs-primary-pressed-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-pressed-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-pressed-icon-color — #262626
--tanrei-comp-tabs-primary-pressed-label-color — #262626
--tanrei-comp-tabs-primary-pressed-selected-active-indicator-color — #1b1b1b
--tanrei-comp-tabs-primary-pressed-selected-container-color — #ffffff · 10%
--tanrei-comp-tabs-primary-pressed-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-primary-pressed-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-primary-pressed-selected-icon-color — #262626
--tanrei-comp-tabs-primary-pressed-selected-label-color — #262626
--tanrei-comp-tabs-primary-pressed-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-primary-pressed-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-pressed-state-layer-color — #262626
--tanrei-comp-tabs-primary-pressed-track-color — #eeeeee
--tanrei-comp-tabs-primary-pressed-underline-color — #000000 · 0%
--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 #1b1b1b
--tanrei-comp-tabs-primary-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-primary-selected-covered-icon-color sys.color.on-primary #ffffff
--tanrei-comp-tabs-primary-selected-covered-label-color sys.color.on-primary #ffffff
--tanrei-comp-tabs-primary-selected-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-primary-selected-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-primary-selected-label-emphasis — 0
--tanrei-comp-tabs-primary-selected-state-layer-color comp.tabs.primary.selected.covered-label.color #ffffff
--tanrei-comp-tabs-primary-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-primary-state-layer-color comp.tabs.primary.label.color #262626
--tanrei-comp-tabs-primary-track-color sys.color.secondary-container #eeeeee
--tanrei-comp-tabs-primary-underline-color — #000000 · 0%
--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 — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-secondary-covered-icon-color sys.color.on-secondary #ffffff
--tanrei-comp-tabs-secondary-covered-label-color sys.color.on-secondary #ffffff
--tanrei-comp-tabs-secondary-disabled-active-indicator-color — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-secondary-disabled-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-active-indicator-color — #1b1b1b · 12%
--tanrei-comp-tabs-secondary-disabled-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-secondary-disabled-selected-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-secondary-disabled-track-color — #eeeeee
--tanrei-comp-tabs-secondary-disabled-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-disabled-underline-shape — 0px
--tanrei-comp-tabs-secondary-disabled-underline-thickness — 2px
--tanrei-comp-tabs-secondary-focused-active-indicator-color — #000000 · 0%
--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 — #262626 · 10%
--tanrei-comp-tabs-secondary-focused-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-focused-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-focused-icon-color — #262626
--tanrei-comp-tabs-secondary-focused-label-color — #262626
--tanrei-comp-tabs-secondary-focused-selected-active-indicator-color — #525252
--tanrei-comp-tabs-secondary-focused-selected-container-color — #ffffff · 10%
--tanrei-comp-tabs-secondary-focused-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-focused-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-focused-selected-icon-color — #262626
--tanrei-comp-tabs-secondary-focused-selected-label-color — #262626
--tanrei-comp-tabs-secondary-focused-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-secondary-focused-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-focused-state-layer-color — #262626
--tanrei-comp-tabs-secondary-focused-track-color — #eeeeee
--tanrei-comp-tabs-secondary-focused-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-focused-underline-shape — 0px
--tanrei-comp-tabs-secondary-focused-underline-thickness — 2px
--tanrei-comp-tabs-secondary-hovered-active-indicator-color — #000000 · 0%
--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 — #262626 · 8%
--tanrei-comp-tabs-secondary-hovered-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-hovered-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-hovered-icon-color — #262626
--tanrei-comp-tabs-secondary-hovered-label-color — #262626
--tanrei-comp-tabs-secondary-hovered-selected-active-indicator-color — #525252
--tanrei-comp-tabs-secondary-hovered-selected-container-color — #ffffff · 8%
--tanrei-comp-tabs-secondary-hovered-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-hovered-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-hovered-selected-icon-color — #262626
--tanrei-comp-tabs-secondary-hovered-selected-label-color — #262626
--tanrei-comp-tabs-secondary-hovered-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-secondary-hovered-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-hovered-state-layer-color — #262626
--tanrei-comp-tabs-secondary-hovered-track-color — #eeeeee
--tanrei-comp-tabs-secondary-hovered-underline-color — #000000 · 0%
--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 #262626
--tanrei-comp-tabs-secondary-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-secondary-pressed-active-indicator-color — #000000 · 0%
--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 — #262626 · 10%
--tanrei-comp-tabs-secondary-pressed-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-pressed-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-pressed-icon-color — #262626
--tanrei-comp-tabs-secondary-pressed-label-color — #262626
--tanrei-comp-tabs-secondary-pressed-selected-active-indicator-color — #525252
--tanrei-comp-tabs-secondary-pressed-selected-container-color — #ffffff · 10%
--tanrei-comp-tabs-secondary-pressed-selected-covered-icon-color — #ffffff
--tanrei-comp-tabs-secondary-pressed-selected-covered-label-color — #ffffff
--tanrei-comp-tabs-secondary-pressed-selected-icon-color — #262626
--tanrei-comp-tabs-secondary-pressed-selected-label-color — #262626
--tanrei-comp-tabs-secondary-pressed-selected-state-layer-color — #ffffff
--tanrei-comp-tabs-secondary-pressed-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-pressed-state-layer-color — #262626
--tanrei-comp-tabs-secondary-pressed-track-color — #eeeeee
--tanrei-comp-tabs-secondary-pressed-underline-color — #000000 · 0%
--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 #525252
--tanrei-comp-tabs-secondary-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-secondary-selected-covered-icon-color sys.color.on-secondary #ffffff
--tanrei-comp-tabs-secondary-selected-covered-label-color sys.color.on-secondary #ffffff
--tanrei-comp-tabs-secondary-selected-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-secondary-selected-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-secondary-selected-label-emphasis — 0
--tanrei-comp-tabs-secondary-selected-state-layer-color comp.tabs.secondary.selected.covered-label.color #ffffff
--tanrei-comp-tabs-secondary-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-secondary-state-layer-color comp.tabs.secondary.label.color #262626
--tanrei-comp-tabs-secondary-track-color sys.color.secondary-container #eeeeee
--tanrei-comp-tabs-secondary-underline-color — #000000 · 0%
--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 — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-tonal-covered-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-tonal-covered-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-tonal-disabled-active-indicator-color — #000000 · 0%
--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 — #000000 · 0%
--tanrei-comp-tabs-tonal-disabled-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-active-indicator-color — #1b1b1b · 12%
--tanrei-comp-tabs-tonal-disabled-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-tonal-disabled-selected-covered-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-covered-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-icon-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-label-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-tabs-tonal-disabled-track-color — #eeeeee
--tanrei-comp-tabs-tonal-disabled-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-disabled-underline-shape — 0px
--tanrei-comp-tabs-tonal-disabled-underline-thickness — 2px
--tanrei-comp-tabs-tonal-focused-active-indicator-color — #000000 · 0%
--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 — #4b4b4b · 10%
--tanrei-comp-tabs-tonal-focused-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-focused-covered-label-color — #262626
--tanrei-comp-tabs-tonal-focused-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-focused-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-focused-selected-active-indicator-color — #262626 · 16%
--tanrei-comp-tabs-tonal-focused-selected-container-color — #262626 · 10%
--tanrei-comp-tabs-tonal-focused-selected-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-focused-selected-covered-label-color — #262626
--tanrei-comp-tabs-tonal-focused-selected-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-focused-selected-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-focused-selected-state-layer-color — #262626
--tanrei-comp-tabs-tonal-focused-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-focused-state-layer-color — #4b4b4b
--tanrei-comp-tabs-tonal-focused-track-color — #eeeeee
--tanrei-comp-tabs-tonal-focused-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-focused-underline-shape — 0px
--tanrei-comp-tabs-tonal-focused-underline-thickness — 2px
--tanrei-comp-tabs-tonal-hovered-active-indicator-color — #000000 · 0%
--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 — #4b4b4b · 8%
--tanrei-comp-tabs-tonal-hovered-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-hovered-covered-label-color — #262626
--tanrei-comp-tabs-tonal-hovered-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-hovered-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-hovered-selected-active-indicator-color — #262626 · 16%
--tanrei-comp-tabs-tonal-hovered-selected-container-color — #262626 · 8%
--tanrei-comp-tabs-tonal-hovered-selected-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-hovered-selected-covered-label-color — #262626
--tanrei-comp-tabs-tonal-hovered-selected-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-hovered-selected-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-hovered-selected-state-layer-color — #262626
--tanrei-comp-tabs-tonal-hovered-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-hovered-state-layer-color — #4b4b4b
--tanrei-comp-tabs-tonal-hovered-track-color — #eeeeee
--tanrei-comp-tabs-tonal-hovered-underline-color — #000000 · 0%
--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 #4b4b4b
--tanrei-comp-tabs-tonal-label-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-tabs-tonal-pressed-active-indicator-color — #000000 · 0%
--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 — #4b4b4b · 10%
--tanrei-comp-tabs-tonal-pressed-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-pressed-covered-label-color — #262626
--tanrei-comp-tabs-tonal-pressed-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-pressed-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-pressed-selected-active-indicator-color — #262626 · 16%
--tanrei-comp-tabs-tonal-pressed-selected-container-color — #262626 · 10%
--tanrei-comp-tabs-tonal-pressed-selected-covered-icon-color — #262626
--tanrei-comp-tabs-tonal-pressed-selected-covered-label-color — #262626
--tanrei-comp-tabs-tonal-pressed-selected-icon-color — #4b4b4b
--tanrei-comp-tabs-tonal-pressed-selected-label-color — #4b4b4b
--tanrei-comp-tabs-tonal-pressed-selected-state-layer-color — #262626
--tanrei-comp-tabs-tonal-pressed-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-pressed-state-layer-color — #4b4b4b
--tanrei-comp-tabs-tonal-pressed-track-color — #eeeeee
--tanrei-comp-tabs-tonal-pressed-underline-color — #000000 · 0%
--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 #262626 · 16%
--tanrei-comp-tabs-tonal-selected-container-color — #000000 · 0%
--tanrei-comp-tabs-tonal-selected-covered-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-tonal-selected-covered-label-color sys.color.on-secondary-container #262626
--tanrei-comp-tabs-tonal-selected-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-tabs-tonal-selected-label-color sys.color.on-surface-variant #4b4b4b
--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 #262626
--tanrei-comp-tabs-tonal-selected-underline-color — #000000 · 0%
--tanrei-comp-tabs-tonal-state-layer-color comp.tabs.tonal.label.color #4b4b4b
--tanrei-comp-tabs-tonal-track-color sys.color.secondary-container #eeeeee
--tanrei-comp-tabs-tonal-underline-color — #000000 · 0%
--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 でスナップショットを撮り、結果を比較できるようにしています。

生成元 specs/tabs.spec.json, tokens/comp/tabs.tokens.json, design/tokens/

ガイドライン

使う場面

1 つの対象をいくつかのビューに分け、1 つずつ見せるときに使います。プロジェクトの「概要」「アクティビティ」「設定」、プロフィールの「投稿」「返信」、ファイルの「プレビュー」「履歴」などです。ビュー同士は対等で、どの順番で行き来してもかまいません。

アプリの行き先、つまり別々の対象の間を移動するには、ナビゲーションバーかドロワーを使います。順番どおりに進める手順や、1 つのリストの絞り込みにはタブを使いません。絞り込みにはチップを使います。タブはビューを切り替えるもので、アクションを実行するものではありません。

セグメントボタンを提供しない Tanrei システムでは、少数の選択肢から 1 つを選ぶことで下に表示する内容が変わる場面でも、セグメントボタンの代わりにタブを使います。カレンダーの上の「日」「週」「月」や、ページの例の上の「標準」「中」「高」などです。こうした並びにパネルは要りません。何を選ぶのかがわかる名前を付けます。切り替える対象のそばで目立たせたくないときは、トーナルのバリアントをトラックなしで使い、ヘッダーの中では小さいサイズにします。フォームと一緒に送る値を決める選択や、目に見えるものを何も変えない選択は、タブにしません。そうした選択にはラジオボタンを使います。

タブ、セグメントボタン、ボタングループの使い分け

コンポーネント どういうものか 用途
タブ 1 つの対象のビュー、または下に表示する内容を変える 1 つの選択です。選択中のタブは 1 つで、矢印キーは移動と同時に選択します。 下に表示する内容の切り替え。「概要」「アクティビティ」「設定」、「日」「週」「月」など。
セグメントボタン 値を決める、つながった 2〜5 個の選択肢です。1 つ選ぶ形と複数選ぶ形があります。既定の Tanrei システムは提供せず、Material と cupertino のプリセットが提供します。 そのプリセットを使うプロダクトでの、決まった少数の答えを持つ設定。
ボタングループ まとめて配置した別々のボタンです。それぞれを個別に押し、グループは選択を持ちません。 関連するアクション。「キャンセル」と「保存」、「太字」「斜体」「下線」など。

簡単な見分け方があります。選ぶと下に表示する内容が変わるなら、パネルがあってもなくてもタブです。フォームと一緒に送る値を決めるならラジオボタン(システムが提供していればセグメントボタン)です。押すと何かが実行されるならボタングループです。

バリアント、サイズ、レイアウトの選び方

選択肢 どうなるか 用途
Primary アイコンは、中サイズではラベルの上に重なり、小サイズではラベルの前に並びます。サイズごとにデザインシステムが決めます(comp.tabs.icon.placement)。インジケーターはプライマリーのバリアントの色で描かれます。 画面の主なビュー。コンテンツの一番上に置きます。
Secondary アイコンはラベルの前に、同じ行に並びます。インジケーターはセカンダリーのバリアントの色で描かれます。 主なビューの中でさらに分けるビュー。プライマリーのタブの下や、パネルの中に置きます。
Tonal インジケーターは淡い層で、選択中のラベルはほかのラベルより濃く、太くなります。アイコンはラベルの前に並びます。 切り替える対象から目を奪いたくない選択。コードサンプルの言語、コントラスト、単位など。
中(size、既定) 並びの高さは 48、インジケーターは 40 です。 画面のコンテンツの一番上に置く並び。
小(size) 並びの高さは 32、インジケーターは 24 です。ラベルの横のパディングとアイコンが小さくなり、どのバリアントでもアイコンはラベルの前に並びます。ラベルの文字の大きさは同じです。 ヘッダーやツールバーの中で、小さいボタンと並べる並び。
中身に合わせる(fullWidth が false、既定) タブの幅を中身に合わせ、どれほど長くても大きくても、ラベルを 1 行のまま省略せずに表示します。トラックは最後のタブで終わり、タブが収まらなければトラックの中で横にスクロールします。インジケーターの幅は選択中のタブの中身に従います。 ほとんどの並び。タブが多いとき、ラベルの長さがまちまちなとき、翻訳されるラベルや利用者が書くラベル。
幅を等分する(fullWidth が true) 並びの幅をタブで等分します。割り当てに収まらないラベルは、末尾を省略記号で省略します。インジケーターはタブの割り当てと同じ幅です。 並びいっぱいに広げたい、2〜5 個の短いラベル。

fullWidth の書き方は、Web では full-width、SwiftUI、Compose、React Native では fullWidth です。Flutter では逆の意味の isScrollable で、Flutter 自身の TabBar と同じく true が既定です。

どちらのレイアウトでも、ラベルが 2 行目へ折り返したり単語の途中で切れたりすることはなく、アイコンがラベルのために縮むこともありません。幅を等分する並びで省略されたラベルも、全体がタブのアクセシブルネームのまま残り、プラットフォームが可能な方法で全体を表示します。Web ではホバーとキーボードフォーカスでのプレーンなツールチップ、Compose では長押しとホバーでのツールチップ、Flutter では Tooltip、SwiftUI では大きなコンテンツビューア(macOS では .help も)です。React Native には独自のツールチップがないので、全体を表示する手段はありません。

3 つのバリアントは 1 つのコントロールです。違うのは選択の重みだけで、キー操作も、ロールも、支援技術に伝える内容も同じです。

デザインシステムはバリアントとサイズを追加できます。追加したバリアントはプライマリーと同じ配置で描かれます。

サイズ

小さい並びは小さいボタンと同じ高さなので、ヘッダーの中で 1 行に並びます。高さは最小ターゲットに届きませんが、そのために並びを高くすることはありません。並びのすぐ上やすぐ下でも、タブと同じ位置を押せば、そのタブを選択できます。そこに置かれたコントロールは、自分への押下を保ちます。並びの下のボタンを押せば、ボタンが押されます。文字を大きくすると、小さい並びも中の並びと同じように高くなり、何も切れません。

トラックとインジケーター

タブとスイッチは 1 つの考え方を共有しています。トラックの上のハンドルが、選ばれたものへ滑り、手で滑らせることもできる、という考え方です。スイッチの 2 つの状態は対等ではないので、状態によって見た目全体が変わります。タブは対等な選択肢なので、トラックは変わらず、動くのはインジケーターだけです。

  • トラック。 並び全体の背後にある、控えめな 1 色の塗りです。両端は完全に丸く、輪郭はありません。高さは並びと同じです。中身に合わせる並びでは、最後のタブから少しの余白を置いて終わります。幅を等分する並びでは、並びと同じ幅です。どのタブが選ばれていても、タブがどの状態でも変わりません。タブが収まらないときは、トラックは動かず、タブがその中でスクロールし、丸い両端で切り取られます。
  • インジケーター。 選択中のタブの中身の背後に描きます。高さは、並びの高さからトラックの上下の余白を引いたもので、どのタブでも同じです。既定のシステムでは、48 の並びで 40、プライマリーのタブがアイコンをラベルの上に重ねる並びで 56 です。中身に合わせる並びでは、タブの中身にパディングを加えた幅です。中身の幅が高さ以下のとき(アイコンだけ、1 文字のラベル、全角 1 文字など)は円になります。幅を等分する並びでは、タブの割り当てと同じ幅です。この決まりはプライマリーでもセカンダリーでも同じです。
  • 3 辺で同じ余白。 インジケーターは、トラックの上端、下端、そして最初と最後のタブではトラックの端から、並びの高さによらず同じ余白を保ちます。インジケーターの丸い端とトラックの丸い端は同心です。インジケーターが円になるタブは、その円と同じ幅になります。最小ターゲットのほうが大きければ、最小ターゲットの幅です。
  • ホバー、フォーカス、押下は、インジケーターがそのタブで占める(選択されていれば占めるはずの)箱の中に描き、タブの矩形全体には描きません。選択中のタブでは、インジケーターの上に、インジケーターが覆う内容の色で描くので、選択中のタブもほかのタブと同じようにホバーと押下に応答します。ポインターに応答するのは、これまでどおりタブ全体です。
  • トラックのない並び。 track を false にすると、並びの背後の塗りを描きません。ほかは何も変わりません。並びの大きさ、余白、各タブの位置、すべての色がそのままなので、トラックのある並びとない並びはぴったり揃います。ヘッダー、カード、コードサンプルの見出しなど、すでに帯になっているものの上に並びを置くときに使います。指定しなければ、track はデザインシステムに従います(comp.tabs.track.visible。既定のシステムでは on)。Web の属性は track="false" です。
  • トーナルのインジケーター。 選択中のラベルの色の半透明の層で、トラックの上にも、並びが置かれたどの面の上にも重ねて描くので、どの面でも同じ差で見えます。意図して控えめにした印なので、選択中のラベルが 2 つ目の印になります。濃くなり、ウェイトが 1 段階太くなるので、選択が色だけで伝わることはありません。どのタブも太いウェイトのラベルが収まる幅をとっておくので、選択が変わっても並びの中で何も動きません。
  • 覆われたラベル。 ラベルとアイコンは、インジケーターに覆われた部分だけが、覆われたときの色で描かれます。インジケーターが滑る間やドラッグされている間、通り過ぎるラベルは覆われた部分だけが変わり、新しいタブのラベルはインジケーターが着いた瞬間にすべて覆われた色になります。覆われたコピーはブレンドではなくシステムの色で描くので、コントラストはトークンが約束するとおりです。

デザインシステムは、選択中のタブの下、並びの下端に線を引くことも、並び全体に区切り線を引くこともできます。どちらもすべてのシステムにあり、既定のシステムでは透明です。Material のプリセットは、Material のタブと同じく、この 2 つに色を付け、トラックとインジケーターを透明にします。

並びの上をドラッグする

  • タップは今までどおり選択する。 タッチ、ペン、マウスで並びの上をドラッグすると、インジケーターがポインターに付いてきます。位置、幅、その下のラベルの覆われた色は、しきい値なしに連続して変わります。
  • 離したときに選ばれる。 インジケーターに近いほうのタブ、またはフリングの向かう先のタブが選ばれます。それが別のタブなら、並びはそのタブを選択して change を 1 回発火します。インジケーターが通り過ぎたタブについては発火しません。選択中のタブなら、インジケーターは元に戻り、何も発火しません。
  • 途中では何も変わらない。 選択、パネル、キーボードフォーカス、支援技術に伝える内容は、離すまで変わりません。Escape を押すか、プラットフォームがポインターを取り消すと、インジケーターは選択中のタブへ戻ります。
  • スクロールは今までどおり。 先に上下へ動いたドラッグは、ページのスクロールです。Web では、タブがスクロールする並びでインジケーターを動かせるのは、選択中のタブから始めたドラッグだけです。それ以外の横方向のドラッグは、タブをスクロールします。
  • 動きを減らす設定。 インジケーターは、その人自身の指が動かすものなので、引き続きポインターに付いていき、離すとすぐにタブの上に置かれます。

トランジション

transition は、並びが新しい選択をどう描くかを指定します。

値 動作
slide インジケーターが新しいタブへ滑りながら大きさを変え、その下でラベルの色が変わります。見えていないタブは、動きを付けてスクロールで見える位置に出します。
none 新しい選択を 1 フレームで全体描きます。何も移動せず、何もフェードしません。
auto(既定) デザインシステムの答えに従います(comp.tabs.motion.transition。既定のシステムでは slide)。

切り替えたばかりの内容のそばにある並びには none を使います。そこでは、滑るインジケーターだけがまだ動いているものになるからです。none でも、並びの上のドラッグはインジケーターを運び、離すとすぐにタブの上に置かれます。ホバー、フォーカス、押下への応答は、どの値でも同じです。動きを減らす設定では、transition が何であっても、選択は none と同じように描かれます。

タブビューは、パネルのための transition を別に持っています。それを none にすると、ビューが描く並びのインジケーターも滑らなくなります。

選択とパネル

  • 値で選ぶ。 どのタブも、並びの中で重複しない value を持ちます。値は、タブを追加、削除、並べ替えしても変わりません。番号で選ぶと、知らないうちに別のビューが選ばれてしまいます。どのタブも指していない value では何も選ばれず、パネルも表示されません。デバッグビルドで報告します。
  • 自動で選択される。 矢印キーでフォーカスを動かすと、そのタブも選択され、パネルはすぐに切り替わります。読み込みに時間のかかるパネルは、切り替えを遅らせるのではなく、パネルの中で読み込み中の状態を表示します。
  • 無効なタブは飛ばす。 矢印キーは無効なタブを飛ばし、無効なタブが Tab で止まる位置になることもありません。フォーカスが移るだけで選択されてしまい、選べないタブを選ぶことになるからです。リストとメニューは無効な項目にも届きますが、そちらはフォーカスが移っても何も選ばれないためです。
  • インジケーターが動く。 選択が変わると、インジケーターは前のタブから新しいタブへ、大きさを変えながら滑るように移動します。円は途中でカプセルへ広がります。動きを減らす設定では瞬時に移動します。並びが最初に現れたときや、選択は変わらずに選択中のタブの大きさが変わったとき(フォントの読み込み、文字サイズの変更など)は、滑らずにその場所へ置かれます。一部がスクロールで隠れたタブが選ばれたりフォーカスを受けたりすると、そのタブが見えるところまで並びをスクロールします。
  • 並びは自分のトラックを描く。 画面やアプリバーのサーフェスの上に置きます。並びのためのコンテナは必要ありません。
  • タブビューの中で。 自分の並びを描くタブビューは、このタブを同じトークンと振る舞いで使います。タブビューをスワイプしている間、タブビューは indicatorOffset でインジケーターを動かします。選択中のタブの上なら 0、次の有効なタブなら 1、前なら −1 で、指の動きに合わせてその間の値をとります。インジケーターは自分の動きを持たず、フレームごとにそれに従います。自分でインジケーターを動かすのでなければ 0 のままにします。タブビューのスワイプと、その並びの上のドラッグは、2 つの場所で行う 1 つのジェスチャーです。スワイプはパネルを 1 つ動かし、パネルも一緒に動きます。並びの上のドラッグは複数のタブを通り過ぎることができ、パネルは指を離すまで待ちます。

選択を共有する

同じ問いに何か所も答えるとき(このサイトのすべてのコードサンプルのプラットフォームや、レポートのすべての数値の単位など)は、それらのタブの並びに同じ group を指定します。ひとつの並びでタブを選ぶと、同じ値のタブをもつほかのすべての並びでもそのタブが選ばれます。その値のタブをもたない並びは、自分の選択を保ちます。

  • 値で対応づける。 グループが追うのはタブの value であり、ラベルではありません。ラベルを翻訳したり言い換えたりしても選択は保たれます。
  • 動くのも知らせるのも、操作された並びだけ。 操作された並びだけがインジケーターをスライドさせ、change を 1 回発火します。ほかの並びは、モーションなしで、フォーカスもスクロールも動かさずにただちに選択を反映し、何も発火しません。上にある並びやパネルの高さが変わっても、操作された並びはウィンドウ内の同じ位置にとどまります。
  • 記憶は別の指定。 rememberAs は、選択をその端末に保存し、並びが最初に描かれる前に復元するためのキーです。キーがなければ何も保存しません。キーは利用者側で決めるもので、指定されたとおりに使われ、保存されるのはタブの値だけです。記憶した設定に同意が必要かどうかは、提供する場面に応じて利用者側で判断してください。
  • Web ではタブがグループを保つ。 group と remember-as は属性で、キーは localStorage のキーです。SwiftUI、Compose、Flutter、React Native では、同じ選択状態を渡された並びどうしがすでに選択を共有しており、その状態の保持と保存は利用者側で行います。

同じグループの 2 つの並び:どちらかでタブを選んでください

マラソンは 42.195 km です。

マラソンは 26.2 マイルです。

早歩きは時速 6 km です。

早歩きは時速 3.7 マイルです。

早歩きは 3.2 ノットです。

このデモでは、「海里」は 2 つ目の並びにしかありません。これを選んでも 1 つ目の並びは自分の選択を保ち、選択が 1 つ目の並びにもある値になると、再び従います。どちらの並びもキーを指定していないので、何も保存されません。

このサイトのタブがその例です。プラットフォーム別のサンプルはすべて、ひとつのグループに属するタブビューです。

縦のタブ

orientation を vertical にすると、タブを開始端の列に縦に積み、切り替えるビューの横に置きます。設定画面やダイアログのカテゴリー、1 件の記録のセクションなどに使います。それ以外は同じコンポーネントです。選択中のタブは 1 つで、インジケーターは滑り、ドラッグでき、パネルも選択の共有も同じように働きます。

パネルの横に並ぶ縦のタブ

言語、地域、最初に開くページ。

メール、プッシュ通知、週ごとのまとめ。

プロフィールを見られる人。

  • 列。 すべてのタブは最も幅の広いタブと同じ幅になり、どのバリアントとサイズでも、アイコンがラベルの前、開始端に並びます。タブは上下で隣と接するので、それぞれ最小ターゲット以上の高さになります。小さいサイズの列のタブは 32 ではなく 44(タッチ入力では 48)の高さです。列を広げるには ::part(tablist) を使います。列が最も広いタブより狭いと、ラベルは省略され、ツールチップで全体を示します。
  • インジケーターとトラック。 インジケーターは、タブからトラックの余白を四辺とも引いた大きさです。トラックの角丸はインジケーターの角丸にその余白を足した値になり、列がどれほど高くても、両端でインジケーターと同心のままです。選択中のタブの下に線を引くデザインシステムでは、その線は列の終端側、パネルの側の辺に引かれ、区切り線もその辺に沿って引かれます。
  • キー。 下矢印と上矢印で次と前のタブに移って選択し、Home と End で両端に移ります。右矢印と左矢印は何もしないので、ページの操作に残ります。支援技術には、タブリストが縦であることを伝えます。
  • スクロール。 並びに高さを与えると、収まらないタブはトラックの中で上下にスクロールします。タッチではスクロールし、インジケーターをドラッグできるのはマウスとペンだけです。fullWidth は列には効きません。

アイコン

  • 1 つの並びでは、すべてのタブにアイコンを付けるか、どれにも付けないかのどちらかにします。アイコンは、選択されていないときはアウトライン、選択されているときは塗りで描かれます。
  • labelHidden を付けるとアイコンだけを描きます。それでもラベルは必須で、タブのアクセシブルネームとして使われます。labelHidden を付けてもアイコンがなければラベルを描き、デバッグビルドで報告します。
  • ラベルが空でアイコンもないタブには中身がありません。インジケーターは空の円になり、タブにはアクセシブルネームがありません。空のラベルはデバッグビルドで報告します。タブにラベルを付けてください。
  • タブに件数を表示したいときは、Tanrei のバッジを利用者側でアイコンに重ねます。タブ自体にバッジのプロパティはありません。

推奨

  • ラベルは「概要」「請求」のように、ビューを表す 1〜2 語の短い言葉にします。幅を等分する並びでは、長いラベルは省略されます。
  • ラベルが長いとき、翻訳されるとき、利用者が書くとき、大きな文字で読まれるときは、既定の「中身に合わせる」のままにします。
  • タブの並び順は変えないでおき、前と同じ場所でビューを見つけられるようにします。
  • セカンダリーのタブはプライマリーのタブのビューの中に置き、その逆にはしません。
  • ときどき使えなくなるタブは、無効にしたまま同じ場所に残し、並びの形を変えないようにします。
  • 1 つの対象のビューが多いとき、またはラベルが長いときは、縦のタブを使います。設定画面のカテゴリーや、記録のセクションなどです。

非推奨

  • アプリの行き先の移動にタブを使わない。それはナビゲーションバーかドロワーの役割です。
  • タブでダイアログを開いたり、アクションを実行したり、画面を離れたりしない。タブは行き先を持ちません。ページの間を移動するには、リンク、ナビゲーションバー、ナビゲーションドロワーを使います。
  • 同じバリアントの並びを入れ子にしない。
  • 1 つの並びで、アイコンのあるタブとないタブを混ぜない。
  • 縦のタブを、アプリの中の行き先を選ぶのに使わない。行き先の列はナビゲーションドロワーの役割です。その項目はリンクで、選ぶとアドレスが変わります。

アクセシビリティ

ロール
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 で止まる位置が 1 つだけで、それは選択中のタブです。右矢印と左矢印で次と前の有効なタブへ移り、端では折り返し、移った先のタブを選択します。右から左(RTL)のレイアウトでは左右が入れ替わります。Home と End で最初と最後の有効なタブへ移ります。Enter と Space でフォーカス中のタブを選択します。Tab を押すと、選択中のタブからそのパネルへ進みます。
  • 無効なタブは矢印キーで飛ばし、Tab で止まる位置にもならず、無効であることを伝えます。
  • フォーカスリングは、フォーカス中のタブでインジケーターが占める箱を、その形のまま囲み、トラックの内側に収めるので、スクロールする並びに切られません。キーボードフォーカスのときだけ表示します。
  • タブは隣のタブと接しているので、最小ターゲットより幅の狭いタブは、最小ターゲットの幅まで広がります。最小ターゲットより低い並び(小さいサイズ)を高くすることはありません。タブは並びの少し上と少し下でも押下に応答し、そこにコントロールがあれば、そのコントロールに譲ります。
  • パネルのない並びは、いくつかの中から 1 つを選ぶものなので、そのとおりに伝えます。label を名前とするラジオグループで、各タブはラジオボタン、選択中のタブはチェックされた状態です。タブリストとして伝えると、スクリーンリーダーの利用者にはパネルが続くように聞こえてしまいます。矢印キーはどれも、並びの向きにかかわらず、ラジオグループと同じく次または前の有効なタブへ移り、そのタブを選択します。その選択が並びの下の何を変えるかは、プロダクトが描きます。Tab を押すと、選択中のタブから並びの次にあるものへ進みます。パネルを持った時点で、並びはタブリストになります。
  • 強制カラーでは、どのバリアントでも、トラックがあってもなくても、インジケーターを塗りつぶしたハイライトで描くので、トーナルの並びもプライマリーと同じようにはっきりと選択を示します。
  • 文字を大きくしても、ラベルが支援技術から隠れることはありません。中身に合わせるタブは幅が広がり、並びがスクロールします。幅を等分するタブはラベルを省略しますが、ラベルはアクセシブルネームのまま残り、プラットフォームが可能な方法で全体を表示します。
  • アイコン、トラック、インジケーター、下線、区切り線は支援技術から隠します。インジケーターの下に描く覆われたラベルのコピーは装飾なので、各ラベルが読み上げられるのは 1 回だけです。
  • 並びの上のドラッグが唯一の手段になることはありません。どのタブも、タップやクリック、矢印キーで選択できます。ドラッグの間は、どのタブも選択中として伝えず、パネルも変わらず、何も読み上げません。選択は指を離したときに行われ、Escape でドラッグを取り消せます。スクリーンリーダーの動作中は、1 本指のドラッグはスクリーンリーダーのもので、タブはその決定操作で選択します。
  • グループでは、従う側の並びは静かに変わります。何も読み上げず、フォーカスは操作されたタブに残り、操作された並びはウィンドウ内の同じ位置にとどまるので、拡大表示を使っている人も場所を見失いません。それぞれの並びは、フォーカスや読み上げがそこへ移ったときに、選択中のタブを支援技術に伝えます。
  • ラベルのないタブ、空の値や重複した値、どのタブも指していない 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:選択中のタブが最初から見えていないスクロールする並びは、そのタブへ移る前に、並びの先頭を 1 フレームだけ表示します。

利用者側で行うこと

  • アイコンだけのタブも含め、すべてのタブにラベルを付けます。
  • 1 つの画面にタブの並びが複数あるときは、label で並びに名前を付けます。「プロジェクト」のように、何のビューなのかが分かる名前にします。
  • 各タブの値は並びの中で重複させず、画面を表示している間は変えないでおきます。
  • Compose と React Native では、選択中のパネルだけを並びのすぐ後ろに描きます。パネルにフォーカスできるものがないときは、パネルをフォーカス可能にし、タブと同じ名前を付けて、Tab で届くようにします。
  • Flutter では、TanreiTabBarView の semanticLabels に各タブのラベルを渡します。
  • React Native では、プロダクトの言語で positionText を渡します。
  • 読み込みに時間のかかるパネルでは、パネルの中に読み込み中の状態を表示します。矢印キーは移動と同時に選択するからです。
  • 同じグループの並びでは、同じ値に同じラベルを付けます。並びどうしが離れているときは、最初の並びの近くで、選択がページ全体に適用されることを伝えます。
  • rememberAs のキーは利用者側で決め、必要な場合はプライバシーに関する説明に記載します。キーがなければ何も保存されません。