---
title: "Navigation bar"
description: "Moves between three to five top-level destinations from the bottom edge of a compact screen, and shows which one is current."
url: "https://tanrei.chnotchy.com/components/navigation-bar/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Navigation bar

> Moves between three to five top-level destinations from the bottom edge of a compact screen, and shows which one is current.

## Overview

> [!NOTE]
> **A revised spec, not yet on every platform**
>
> This page describes the navigation bar as the spec has it now, on the model of the [tabs](https://tanrei.chnotchy.com/components/tabs/). The active indicator is the current destination’s whole equal share of the bar, held in from the bar’s edges, not a pill that hugs the icon and label. It slides to the new destination at its full size when the selection changes, and a drag along the bar carries it with the pointer. Hover, press and focus are drawn in that same box. `variant` chooses between tonal (the default) and filled. Every other prop and event is the same in both revisions. The platforms marked lagging above ship the first revision until they catch up: no `variant`, no drag, and an indicator behind the icon alone that grows in from its centre, and their samples show that revision. The demos on this page are the web component as it ships today.

### Usage

A navigation bar has one kind of item, `destination`: an icon, a label under it, and an optional badge on the icon. Each platform takes the destinations the way it takes any repeated children: `<tanrei-navigation-bar-item>` child elements on the web, `TanreiNavigationBarItem` views in SwiftUI’s view builder, `TanreiNavigationBarItem` calls in Compose’s content lambda, `TanreiNavigationDestination` objects in Flutter’s `destinations:` list, and a `destinations` array of plain objects on React Native.

The selection belongs to the bar, by a destination’s value. `value` names the current destination, `change` carries the one the person chose, and `reselect` fires when they choose the current one again, so you can scroll that screen back to its top. An empty value selects none, for a screen that is not one of the bar’s destinations. SwiftUI writes the new value to the `selection` binding. Two platforms keep their own idiom, as their own navigation bars do. Compose has no bar-level value: you mark the current item `selected`, and its `onClick` serves as both change and reselect. Flutter selects by index: `selectedIndex`, and `onDestinationSelected`, which it calls for the current destination too.

`variant` sets how strongly the current destination is marked: `tonal` (the default) fills its indicator softly, and `filled` fills it strongly, with the icon and label inverted on it. It is on the web today.

A destination with an `href` is a link: the platform’s own link, drawn exactly as a button destination. Enter activates it and Space does not. The bar does not select a link on a press: it emits `change` with the destination’s value and its `href`, and then the link is followed. The page that arrives names the current destination through `value`, as in the [navigation drawer](https://tanrei.chnotchy.com/components/navigation-drawer/), and a link without a `value` takes its `href` as its value. `target` and `targetLabel` are the [Button](https://tanrei.chnotchy.com/components/button/)’s. The web builds link destinations; SwiftUI, Compose, Flutter and React Native do not have them yet.

A destination’s `icon` is the name of a glyph in the design system’s icon font. The bar draws it outlined while unselected and filled while selected. `badge`, `badgeCount` and `badgeLabel` draw a Tanrei badge on the icon: a dot when there is no count, otherwise the number. SwiftUI, Compose and Flutter take them as one `TanreiNavigationBadge` value, and React Native as a `badge` object.

The bar is a layout region, not an overlay. Its color reaches under the home indicator or gesture bar below it, and its destinations sit above them and inside the side insets. On React Native the insets come from props only, with no safe-area dependency: `TanreiProvider`’s `insets`, or the bar’s own `insets`, whose sides replace the provider’s.

**Web**

```html
<!-- The host is a navigation landmark named by label. -->
<!-- variant="filled" inverts the current destination, for a bar over a busy screen. Tonal is the default. -->
<tanrei-navigation-bar label="Main" value="inbox">
  <tanrei-navigation-bar-item value="inbox" label="Inbox" icon="inbox"
                              badge badge-count="3" badge-label="3 unread"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="chat" label="Chat" icon="chat"
                              badge badge-label="New messages"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="starred" label="Starred" icon="star"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item value="settings" label="Settings" icon="settings" disabled></tanrei-navigation-bar-item>
</tanrei-navigation-bar>

<script type="module">
  import '@tanrei/web/components/navigation-bar';
  const bar = document.querySelector('tanrei-navigation-bar');
  // The bar sets its own value before it fires change, which carries it.
  bar.addEventListener('change', (e) => showScreen(e.detail.value));
  bar.addEventListener('reselect', () => scrollTo({ top: 0 }));
</script>

<!-- A bar of links: each page sets value to its own address. A link without a value takes its href. -->
<tanrei-navigation-bar label="Main" value="/search/">
  <tanrei-navigation-bar-item href="/" label="Home" icon="home"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="/search/" label="Search" icon="search"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="/library/" label="Library" icon="video_library"></tanrei-navigation-bar-item>
  <tanrei-navigation-bar-item href="https://example.com/help" label="Help" icon="help"
                              target="new" target-label="opens in a new tab"></tanrei-navigation-bar-item>
</tanrei-navigation-bar>

<script type="module">
  // A router takes the navigation: cancel change, go to detail.href, and say where the product is now.
  bar.addEventListener('change', (e) => {
    if (!e.detail.href) return;
    e.preventDefault();
    router.push(e.detail.href);
    bar.value = e.detail.value;
  });
</script>
```

**React**

```tsx
import { NavigationBar, NavigationBarItem } from '@tanrei/react';

// variant="filled" for a bar over a busy screen; tonal is the default.
<NavigationBar label="Main" value={screen} onChange={(e) => setScreen(e.detail.value)} onReselect={scrollToTop}>
  <NavigationBarItem value="inbox" label="Inbox" icon="inbox" badge badgeCount={3} badgeLabel="3 unread" />
  <NavigationBarItem value="chat" label="Chat" icon="chat" />
  <NavigationBarItem value="starred" label="Starred" icon="star" />
</NavigationBar>
```

**SwiftUI**

```swift
import Tanrei

// A bar of its own in the bottom safe-area inset. Choosing a destination writes the binding (change).
ScreenView(screen)
    .safeAreaInset(edge: .bottom, spacing: 0) {
        TanreiNavigationBar(selection: $screen, accessibilityLabel: "Main", onReselect: { _ in scrollToTop() }) {
            TanreiNavigationBarItem("Inbox", icon: "inbox", value: "inbox",
                                    badge: TanreiNavigationBadge(count: 3, label: "3 unread"))
            TanreiNavigationBarItem("Chat", icon: "chat", value: "chat")
            TanreiNavigationBarItem("Starred", icon: "star", value: "starred")
            // disabled is SwiftUI's own modifier.
            TanreiNavigationBarItem("Settings", icon: "settings", value: "settings").disabled(true)
        }
    }
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiNavigationBadge
import io.github.chnotchy.tanrei.components.TanreiNavigationBar
import io.github.chnotchy.tanrei.components.TanreiNavigationBarItem

// As Material's NavigationBar: no bar-level value. You mark the current item selected,
// and one onClick is both change and reselect.
var screen by rememberSaveable { mutableStateOf("inbox") }
TanreiNavigationBar(contentDescription = "Main") {
    TanreiNavigationBarItem(
        selected = screen == "inbox",
        onClick = { if (screen == "inbox") { scrollToTop() } else { screen = "inbox" } },
        icon = "inbox",
        label = "Inbox",
        badge = TanreiNavigationBadge(count = 3, contentDescription = "3 unread"),
    )
    TanreiNavigationBarItem(selected = screen == "chat", onClick = { screen = "chat" }, icon = "chat", label = "Chat")
    // enabled is the inverse of the spec's disabled.
    TanreiNavigationBarItem(selected = screen == "settings", onClick = { screen = "settings" }, icon = "settings", label = "Settings", enabled = false)
}
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

// As NavigationBar: a destination's value is its index. The bar keeps no selection of its own.
TanreiNavigationBar(
  selectedIndex: current, // null selects none
  onDestinationSelected: (int index) {
    if (index == current) {
      scrollToTop(); // reselect
    } else {
      setState(() => current = index); // change
    }
  },
  semanticLabel: 'Main',
  destinations: const <TanreiNavigationDestination>[
    TanreiNavigationDestination(icon: 'inbox', label: 'Inbox', badge: TanreiNavigationBadge(count: 3, semanticLabel: '3 unread')),
    TanreiNavigationDestination(icon: 'chat', label: 'Chat'),
    TanreiNavigationDestination(icon: 'settings', label: 'Settings', enabled: false),
  ],
),
```

**React Native**

```tsx
import { useState } from 'react';
import { NavigationBar } from '@tanrei/react-native';

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

// Controlled: move value from onValueChange.
<NavigationBar
  accessibilityLabel="Main"
  value={screen}
  onValueChange={setScreen}
  onReselect={scrollToTop}
  destinations={[
    { value: 'inbox', label: 'Inbox', icon: 'inbox', badge: { count: 3, label: '3 unread' } },
    { value: 'chat', label: 'Chat', icon: 'chat', badge: { label: 'New messages' } },
    { value: 'settings', label: 'Settings', icon: 'settings', disabled: true },
  ]}
/>
```

## Specs

- Category: Navigation
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/navigation-bar/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/navigation-bar.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | Variants left out: `filled`. |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | Variants left out: `filled`. |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | Variants left out: `filled`. |

### Platforms

- Web: the element `<tanrei-navigation-bar>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/navigation-bar`).
- React: `NavigationBar` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-navigation-bar` | lagging |
| SwiftUI | `TanreiNavigationBar` | lagging |
| Jetpack Compose | `TanreiNavigationBar` | lagging |
| Flutter | `TanreiNavigationBar` | lagging |
| React Native | `NavigationBar` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `value` (prop) | `value` | `selection` | `selected` | `selectedIndex` | `value` |
| `labelVisibility` (prop) | `labelVisibility` | `labelVisibility` | `labelVisibility` | `labelBehavior` | `labelVisibility` |
| `label` (prop) | `label` | `accessibilityLabel` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `change` (event) | `change` | `selection` | `onClick` | `onDestinationSelected` | `onValueChange` |
| `reselect` (event) | `reselect` | `onReselect` | `onClick` | `onDestinationSelected` | `onReselect` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

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

### Props

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

### Events

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

### Item: `destination`

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

Names: Web `tanrei-navigation-bar-item`, SwiftUI `TanreiNavigationBarItem`, Jetpack Compose `TanreiNavigationBarItem`, Flutter `TanreiNavigationDestination`, React Native `NavigationBarDestination`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | `""` | `value` | What identifies the destination. The bar's value selects the destination whose value equals it; change and reselect carry it. Unique within the bar. Empty on a link, its href stands as its value. |
| `href` | `string` | `""` | `href` | The address the destination leads to. Not empty, the destination is a link and not a button (NVB-30): it is followed as the platform follows a link, and the bar does not select it by itself (NVB-31). Empty, the destination is a button and the product moves on change. As the Button's href (a control with a destination is a link). |
| `target` | `enum`: `same`, `new` | `"same"` | `target` | Where a link opens, as the Button's target. same: where the person is. new: a new context — a new tab or window on the web, outside the current screen elsewhere — which a bar's cell has no place to mark and says through targetLabel (NVB-32). A link that opens elsewhere is never selected by a press (NVB-31). Ignored without href. |
| `targetLabel` | `string` | `""` | `target-label` | What assistive technology reads after the link's name while target is new ("opens in a new tab"), as the Button's targetLabel: Tanrei ships no words of its own. Required while target is new; ignored otherwise. |
| `label` | `string` | `""` | `label` | Its name, drawn under the icon (see the bar's labelVisibility) and always its accessible name. Required. |
| `icon` | `string` | `""` | `icon` | The name of its glyph in the system's icon set, drawn in the destination's own icon style: outlined while unselected, filled while selected. Required. |
| `disabled` | `boolean` | `false` | `disabled` | Makes it unavailable. It keeps its place, draws its disabled look, cannot be activated and leaves the focus order. |
| `badge` | `boolean` | `false` | `badge` | Draws a Tanrei badge (the badge component) on the icon, from badgeCount and badgeLabel. |
| `badgeCount` | `number` | — | `badge-count` | The badge's count, as the badge component's count: null draws the dot, 0 draws nothing, above 0 the number (above 999 as 999+). Ignored while badge is false. |
| `badgeLabel` | `string` | `""` | `badge-label` | The badge's label, as the badge component's: what assistive technology reads after the destination's name and state ("3 unread"). A dot needs one. Ignored while badge is false. |

### States

`enabled`, `disabled`, `hovered`, `focused`, `pressed`, `selected`

### Accessibility

- Role: `navigation`
- Labeling: Each destination is named by its label, drawn or not; its icon is decorative, and so is the covered copy of its icon and label. A badge adds its description (badgeLabel, or the displayed count) after the name, and a link that opens a new context its targetLabel after that. The bar's label names the bar itself.
- Minimum target size: yes

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

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

### Behaviour

Requirements in EARS form, each with its stable id.

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

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.navigation-bar.…` → `--tanrei-comp-navigation-bar-…`).

#### `tanrei.comp.navigation-bar`

| Token | Type | Value |
| --- | --- | --- |
| `comp.navigation-bar.container.color` | `color` | `{tanrei.sys.color.surface}` |
| `comp.navigation-bar.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.navigation-bar.container.height` | `dimension` | `64px` |
| `comp.navigation-bar.container.padding` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.navigation-bar.outline.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.navigation-bar.outline.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.navigation-bar.item.container.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.navigation-bar.item.container.padding-inline` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.navigation-bar.item.container.padding-block` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.navigation-bar.item.icon-indicator.width` | `dimension` | `{tanrei.comp.navigation-bar.item.icon.size}` |
| `comp.navigation-bar.item.icon-indicator.height` | `dimension` | `{tanrei.comp.navigation-bar.item.icon.size}` |
| `comp.navigation-bar.item.icon-indicator.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.navigation-bar.item.icon.size` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.navigation-bar.item.label.typescale` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.navigation-bar.item.gap` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.navigation-bar.unselected.item.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.navigation-bar.unselected.item.icon-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.navigation-bar.unselected.item.icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.navigation-bar.unselected.item.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.navigation-bar.unselected.item.covered-icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.navigation-bar.unselected.item.covered-label.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.navigation-bar.unselected.item.state-layer.color` | `color` | `{tanrei.comp.navigation-bar.unselected.item.icon.color}` |
| `comp.navigation-bar.selected.item.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.navigation-bar.selected.item.icon-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.navigation-bar.selected.item.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.navigation-bar.selected.item.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.navigation-bar.selected.item.covered-icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.navigation-bar.selected.item.covered-label.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.navigation-bar.selected.item.state-layer.color` | `color` | `{tanrei.comp.navigation-bar.selected.item.icon.color}` |
| `comp.navigation-bar.filled-unselected.item.container.color` | `color` | `{tanrei.comp.navigation-bar.unselected.item.container.color}` |
| `comp.navigation-bar.filled-unselected.item.icon-indicator.color` | `color` | `{tanrei.comp.navigation-bar.unselected.item.icon-indicator.color}` |
| `comp.navigation-bar.filled-unselected.item.icon.color` | `color` | `{tanrei.comp.navigation-bar.unselected.item.icon.color}` |
| `comp.navigation-bar.filled-unselected.item.label.color` | `color` | `{tanrei.comp.navigation-bar.unselected.item.label.color}` |
| `comp.navigation-bar.filled-unselected.item.covered-icon.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.navigation-bar.filled-unselected.item.covered-label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.navigation-bar.filled-unselected.item.state-layer.color` | `color` | `{tanrei.comp.navigation-bar.filled-unselected.item.icon.color}` |
| `comp.navigation-bar.filled-selected.item.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.navigation-bar.filled-selected.item.icon-indicator.color` | `color` | `{tanrei.comp.navigation-bar.selected.item.icon-indicator.color}` |
| `comp.navigation-bar.filled-selected.item.icon.color` | `color` | `{tanrei.comp.navigation-bar.selected.item.icon.color}` |
| `comp.navigation-bar.filled-selected.item.label.color` | `color` | `{tanrei.comp.navigation-bar.selected.item.label.color}` |
| `comp.navigation-bar.filled-selected.item.covered-icon.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.navigation-bar.filled-selected.item.covered-label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.navigation-bar.filled-selected.item.state-layer.color` | `color` | `{tanrei.comp.navigation-bar.filled-selected.item.covered-icon.color}` |
| `comp.navigation-bar.motion.state-duration` | `duration` | `{tanrei.sys.motion.role.state.large}` |
| `comp.navigation-bar.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.navigation-bar.motion.feedback-duration` | `duration` | `{tanrei.sys.motion.role.feedback}` |
| `comp.navigation-bar.motion.selection-duration` | `duration` | `{tanrei.sys.motion.role.selection.travel}` |
| `comp.navigation-bar.motion.selection-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.spatial}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.navigation-bar.<variant>.<state>.<property>` for the variants `unselected`, `selected`, `filled-unselected`, `filled-selected`, the states `hovered`, `focused`, `pressed`, `disabled` and the properties `item.container.color`, `item.icon-indicator.color`, `item.icon.color`, `item.label.color`, `item.covered-icon.color`, `item.covered-label.color`, `item.state-layer.color`.
- `comp.navigation-bar.<state>.<property>` for the states `hovered`, `focused`, `pressed`, `disabled` and the properties `opacity`.

## Guidelines

### When to use

Use a navigation bar on a compact screen, such as a phone held upright, to move between three to five top-level destinations: the places a person returns to all day, such as Inbox, Search and Profile. The bar stays on every top-level screen and shows where the person is.

Use tabs to switch between views of one subject within a screen. Use an app bar’s back button to leave a screen a person reached from another one: detail screens usually hide the navigation bar.

### Choosing between the bar and the drawer

| Aspect | [Navigation bar](https://tanrei.chnotchy.com/components/navigation-bar/) | [Navigation drawer](https://tanrei.chnotchy.com/components/navigation-drawer/), collapsed | [Navigation drawer](https://tanrei.chnotchy.com/components/navigation-drawer/), expanded |
| --- | --- | --- | --- |
| Where it sits | Along the bottom edge | A column of icons along the start edge | In a panel at the start edge |
| Window | Compact | Medium and wider | Expanded and wider (standard), or compact (modal) |
| Destinations | Three to five | Its top-level destinations and groups, one cell each | As many as the product has, in sections and groups |
| A destination | An icon over a one-line label, with a badge on the icon | The row cut after its icon: the icon alone, with a badge on it and its name in a tooltip | A row: an optional icon, a label that wraps, and trailing text or content at its end |
| It also holds | Nothing else | A header: the menu button and the main action, collapsed to its icon | A header: the menu button, a title or an account switcher, and the main action |
| On native platforms, a destination is | A tab of a tab bar | A button with a selected state | A button with a selected state |
| What it is | A layout region | A layout region, or a column in the layout that a modal drawer widens from | Standard: a layout region. Modal: a modal overlay |

- **The collapsed drawer is the navigation rail.** A collapsible drawer narrows to a column of icons where the content needs the width, and its menu button widens it again; every icon stays where it is. Tanrei’s separate navigation rail is retired into this form.
- **Bar and drawer are one set of destinations in two places.** They take the same destinations and the same selection, so a product that adapts to the window swaps the bar for the drawer at a breakpoint without touching its destinations. Show one of them at a time, never both.
- **The drawer holds more.** Use it expanded when the product has more destinations than a bar can hold, or when they fall into groups, such as a mail app’s folders and labels. On a large window a standard drawer sits beside the content. On a compact one a modal drawer slides over it from a menu button.
- **A modal collapsible drawer** keeps its column of icons in the layout and widens over the content, above a scrim, when its menu button opens it.
- On every platform, both select by a destination’s value (Compose by a per-item flag, Flutter by index) and emit `change` and `reselect`.

### Labels

| `labelVisibility` | What it does | Use for |
| --- | --- | --- |
| `always` (default) | Every destination draws its label. | Most bars. Labels make destinations recognisable at a glance. |
| `selected` | Only the current destination draws its label. An icon without a label sits in the middle of its destination. No destination changes size or place with the selection. | Bars where space is tight and the icons are well known. |
| `never` | No destination draws its label. | Media and social products whose four or five icons are universally known. |

A label that is not drawn is still the destination’s accessible name, so every destination needs one.

### Selection and badges

- **The active indicator.** Every destination has one box: its equal share of the bar’s width, held in from the bar’s edges, above and below and at the two ends, by one inset (4 in the default system). Four destinations in a bar 360 wide and 64 high each have a box of 88 × 56, a capsule in the default system. The box is the same for every destination of a bar, whatever it holds, and is never sized to its icon and label. The current destination’s box is filled, which makes it the active indicator, and its icon is filled. Hover, focus and press are drawn in that same box for every destination.
- **The slide.** When the selection changes, the indicator slides from the old destination to the new one at its full size. The icons and labels are inverted only where the indicator covers them, so a destination it passes over changes colors under it for a moment, and the new destination is wholly inverted when the indicator lands. From no selection the indicator fades in on the new destination; to no selection it fades out. It never grows or shrinks. When the bar first appears, or the current destination changes size without a new selection (a font arriving, the text size changing), the indicator is put in place at once. Where motion is reduced, the change happens at once.
- **The drag.** A press on the bar that moves sideways, with touch, a pen or a mouse, while a destination is selected, carries the indicator with the pointer. Nothing is chosen until the release: `value`, focus, each icon’s fill and what is announced stay with the current destination. The release chooses the nearer of the two destinations the indicator lies between, or the one a fling is toward, and fires `change` once. Released back on the current destination, it fires nothing, `reselect` included. Escape cancels the drag. A drag is never the only way: a tap, a click, Enter and Space choose as before.
- **Reselect.** Choosing the current destination again fires `reselect`, not `change`. The usual answer is to scroll that screen to its top, or to go back to its first page.
- **Badges.** A badge with a count shows the number (999+ above 999), and one without a count shows a dot. `badgeLabel` is what a screen reader says after the destination’s name, such as “3 unread”. A dot has no number to read, so it always needs one. A badge stays while its destination is disabled.

### Links

In a site of separate pages, give every destination an `href` and set the bar’s `value` to the page’s own address. The current destination and the current page are then one thing, with one mark.

- **Followed, not selected.** A plain activation of a link emits `change`, or `reselect` on the current one, with the destination’s value and its `href`, and then the link is followed. The bar does not write `value`: the active indicator stays on the destination that `value` names and moves when `value` changes.
- **A router.** Cancel the event to take the navigation yourself, go to `detail.href` and set `value`. The indicator then slides to the new destination.
- **Other tabs.** A click or Enter with a modifier key, a middle click, a link whose `target` is `new` and a download are the browser’s. The bar emits nothing and selects nothing.
- **A new context.** `target: new` opens a new tab or window. A destination is an icon over a label, with no place for a mark, so none is drawn: `targetLabel` says it to assistive technology, after the badge’s description.
- **The drag.** A sideways drag that begins on a link moves the active indicator and does not drag the link’s address. Released on a link, it is a plain press on that link: the indicator returns to the current destination until `value` changes.
- **Disabled.** A disabled link cannot be followed by any means and leaves the focus order.

### Tonal and filled

| `variant` | The current destination | Use for |
| --- | --- | --- |
| `tonal` (default) | A soft indicator in the secondary container color, its icon and label in that container’s content color. | Most bars. |
| `filled` | A strong indicator in the primary color, its icon and label inverted on it. | A bar that must stand out from a busy screen: a map, a photo grid, a player. |

Destinations the indicator does not cover look the same in both. The variant changes how strongly the selection is drawn, not what is announced: a tonal and a filled bar expose the same names, roles and states. A disabled current destination keeps its tonal indicator with disabled content on it; a filled indicator fades as a disabled filled Button does, since disabled content on the primary color would be hard to read.

The `m3-like` and `cupertino` presets leave `filled` out, as Material’s navigation bar and Apple’s tab bar have one style. `m3-like` keeps Material’s box behind the icon alone, with the label below it and no box around the destination, and `cupertino` marks the current tab with its tint only. Under both, a drag shows nothing moving, and its release still chooses.

### Do

- Keep labels to one short word: “Inbox”, “Search”, “Profile”. A label that does not fit its destination is cut at its end with an ellipsis, and nothing wraps.
- Use `filled` only where the tonal indicator would be lost against the screen behind the bar.
- Keep the destinations and their order the same on every top-level screen.
- Use the same icon style for every destination, and let the bar draw the filled form for the current one.

### Don’t

- Don’t use the bar for actions such as “Create” or “Share”. Use a main action button or the app bar.
- Don’t put fewer than three or more than five destinations in it. Debug builds report the count. Move the rest to a navigation drawer.
- Don’t hide a destination that is sometimes unavailable. Disable it, so the bar keeps its shape.
- Don’t show a badge on every destination at once.

## Accessibility

### What the component handles

- On the web the bar is a navigation landmark named by `label`. Each destination is a link (with `href`) or a button, and the current one carries `aria-current="page"`, on a link and on a button alike: there is no second state for a current page. It is not a tab list: a switch between whole screens has no tab panels and no arrow-key movement.
- On iOS, Android, Flutter and React Native the bar is the platform’s tab bar, as their own bottom bars are. Each destination is one element with the tab role (on SwiftUI, a button with the selected trait), its selected state and its position, named by its label whether or not the label is drawn. A screen reader says, for example, “Inbox, tab, 1 of 4, selected, 3 unread”.
- The icon is hidden from assistive technology. A badge adds its description (`badgeLabel`, or the number it shows) after the name and state, and a link that opens a new context adds its `targetLabel` after that.
- Each destination that is not disabled is its own Tab stop, as the links of a navigation region are. Enter and Space activate the focused destination (Enter only, for a link on the web). Disabled destinations take no focus and are announced as disabled.
- The focus ring is drawn around the destination’s box and held inside the bar, for keyboard focus only.
- A drag of the indicator changes nothing that is announced until it is released, and never moves focus.
- The inverted copy of each icon and label that the indicator covers is decorative: assistive technology meets each destination once, and `variant` does not change what is announced.
- The whole cell is the hit area: the bar’s full height and an equal share of its width, at least the minimum target. A bar too narrow for its destinations still shares its width equally: the cells narrow below the target rather than make the bar wider than its space.
- Larger text grows the bar, and every destination’s box with it, so that the labels still fit on one line. The icons are not scaled. A label that is truncated stays whole as the destination’s name. Where iOS offers the large-content viewer, a long press shows the destination’s icon and label enlarged.
- Debug builds report fewer than three or more than five destinations, a destination without a label, a value that names no destination, and a value used twice. They also report a link that opens a new context without a `targetLabel`, an `href` that names a script or cannot be read, and a `target` the bar does not define.

Known platform gaps:

- SwiftUI, Compose, Flutter and React Native have no link destinations: a destination takes no `href` there yet.
- Web: every size follows `rem`, so a larger browser text size also grows the item containers and icons. Enlarging Tanrei’s type alone does not.
- SwiftUI: the cells’ minimum is the platform’s own target, 44 points on macOS, where the spec names the 48-point touch target.
- SwiftUI: outside iOS, a destination carries its position as its value text (“2 of 4”), in English only. An image icon, such as an SF Symbol, does not follow the selection: pass the filled symbol for the selected destination yourself. The large-content viewer is built in but not yet exercised by a test.
- Compose has no bar-level value, so a value that names no destination has nothing to report: mark no item selected to select none.
- Flutter’s semantics have no position in the set, so “1 of 4” is not announced.
- SwiftUI, Flutter and React Native switch the icon’s family and fill at once instead of over the selection motion.
- React Native has no large-content viewer. A destination carries its position as its value text (“1 of 4”), in English unless you pass `positionText`.
- React Native: the selection motion, the insets and what TalkBack and VoiceOver announce have not yet been checked on a device.
- Every platform, default system: the tonal indicator stands about 1.15:1 against the bar, and the iOS accessibility audit flags it. The filled icon and the label’s color also mark the selection, and `filled` gives an indicator of at least 3:1 where that matters.

### What you need to do

- Give every destination a label, including in a bar that draws none.
- Name the bar with `label`, such as “Main”, especially when the screen has another navigation region.
- Give every badge a `badgeLabel` that says what it counts, and always one for a dot.
- Keep each destination’s value unique and stable.
- In a site of separate pages, set `value` on every page to that page’s destination. The bar does not select a link by itself.
- Give a link that opens a new tab a `targetLabel`, in your product’s language, such as “opens in a new tab”. Tanrei ships no words of its own.
- On Compose, mark exactly one item `selected`, or none. On Flutter, compare the index you receive with `selectedIndex` to tell a reselect from a change.
- On React Native, pass `positionText` in your product’s language, and give `TanreiProvider` the window’s insets.
