---
title: "Toolbar"
description: "Holds a set of related controls as one Tab stop, inline where its content needs it, docked across the bottom edge, or floating over the content. The arrow keys move between its controls, and what does not fit goes into an overflow menu."
url: "https://tanrei.chnotchy.com/components/toolbar/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Toolbar

> Holds a set of related controls as one Tab stop, inline where its content needs it, docked across the bottom edge, or floating over the content. The arrow keys move between its controls, and what does not fit goes into an overflow menu.

## Overview

### Usage

A toolbar holds a set of related controls: actions, toggles, a button that opens a menu, a [button with menu](https://tanrei.chnotchy.com/components/button-with-menu/). Its items are your own Tanrei [Buttons](https://tanrei.chnotchy.com/components/button/) and buttons with menu, each with its own label, click and state. The toolbar lays them out in reading order, sets groups apart, moves keyboard focus among them and hides what does not fit into an overflow menu. It never restyles them, so an icon-only Button in a toolbar looks and behaves as it does anywhere else.

It has six kinds of item:

- `action`: an icon-only Button. A toggle such as Bold is one with `toggle` set.
- `button`: a Button with a label, which may be a toggle too, such as a filter that is on or off.
- `menu-button`: a Button with a Tanrei [Menu](https://tanrei.chnotchy.com/components/menu/) anchored to it, such as Sort or Insert. The menu is yours and stands beside its Button. The toolbar does not lay it out.
- `button-with-menu`: a main action with a menu of related ones. Each half is its own stop of the arrow keys.
- `group`: a run of related items, such as Bold, Italic and Underline. It draws nothing of its own. Give it a `label` when the grouping means something to the person, such as “Text style”.
- `separator`: a line between two items that are not in groups. On the web it is a [divider](https://tanrei.chnotchy.com/components/divider/), which the toolbar draws as it draws the line between two groups.

**Inline (default).** An inline toolbar stands where your layout puts it, in the content it acts on: above a table, at the top of an editor, beside a list as a row of filters. It is as long as its items need and never longer than the layout allows. In the default system it has no surface, shadow or edge of its own, and its ends line up with the content around it.

**Docked.** A docked toolbar spans the bottom edge of the screen. Its color reaches under the home indicator or gesture bar, and its items stay above it and inside the side insets. The web reads `env(safe-area-inset-bottom)`, SwiftUI places the bar with `.safeAreaInset(edge: .bottom)`, Compose takes `windowInsets` (the navigation bars by default), and Flutter reads the bottom `MediaQuery` padding. React Native takes its insets from the toolbar’s `insets` prop or from `TanreiProvider`, since Tanrei takes no safe-area dependency.

**Floating.** A floating toolbar does not place itself, as a [main action button](https://tanrei.chnotchy.com/components/main-action-button/) does not. Your layout puts it, normally centered above the bottom of the content, or beside the content’s end edge when it is vertical.

The Web and React samples show the current spec. SwiftUI, Compose, Flutter and React Native are lagging. They still ship the previous revision of the toolbar and of the button, where a toolbar is docked unless you say otherwise, holds only Buttons, scrolls what does not fit, and an icon-only action is a separate icon button. Their samples show that revision until they catch up.

**Web**

```html
<tanrei-toolbar label="Formatting" overflow-label="More formatting">
  <tanrei-toolbar-group label="Text style">
    <tanrei-button variant="text" toggle selected label="Bold"><span slot="icon">format_bold</span></tanrei-button>
    <tanrei-button variant="text" toggle label="Italic"><span slot="icon">format_italic</span></tanrei-button>
  </tanrei-toolbar-group>
  <tanrei-toolbar-group>
    <tanrei-button variant="text" label="Insert link"><span slot="icon">link</span></tanrei-button>
    <tanrei-button variant="text" id="insert" label="Insert"><span slot="icon">add</span></tanrei-button>
    <tanrei-menu anchor="insert">
      <tanrei-menu-item label="Table"></tanrei-menu-item>
      <tanrei-menu-item label="Horizontal line"></tanrei-menu-item>
    </tanrei-menu>
  </tanrei-toolbar-group>
  <tanrei-divider></tanrei-divider>
  <tanrei-button variant="filled" toolbar-overflow="never">Publish</tanrei-button>
</tanrei-toolbar>

<!-- Docked across the bottom edge of the screen. -->
<tanrei-toolbar variant="docked" label="Message actions">
  <tanrei-button variant="text" label="Archive"><span slot="icon">archive</span></tanrei-button>
  <tanrei-button variant="text" label="Delete"><span slot="icon">delete</span></tanrei-button>
  <tanrei-button variant="filled">Reply</tanrei-button>
</tanrei-toolbar>

<script type="module">
  import '@tanrei/web/components/toolbar';
  import '@tanrei/web/components/button';
  import '@tanrei/web/components/divider';
  import '@tanrei/web/components/menu';
  // The items are yours: the toolbar adds no events of its own.
  document.querySelector('[label="Insert link"]').addEventListener('click', insertLink);
  const insert = document.querySelector('tanrei-menu[anchor="insert"]');
  document.querySelector('#insert').addEventListener('click', () => (insert.open = !insert.open));
  insert.addEventListener('close', () => (insert.open = false));
</script>
```

**React**

```tsx
import { Button, Divider, Toolbar, ToolbarGroup } from '@tanrei/react';

<Toolbar label="Formatting" overflowLabel="More formatting">
  <ToolbarGroup label="Text style">
    <Button variant="text" toggle selected={bold} label="Bold" onChange={(e) => setBold(e.detail.selected)}><span slot="icon">format_bold</span></Button>
    <Button variant="text" toggle selected={italic} label="Italic" onChange={(e) => setItalic(e.detail.selected)}><span slot="icon">format_italic</span></Button>
  </ToolbarGroup>
  <ToolbarGroup>
    <Button variant="text" label="Insert link" onClick={insertLink}><span slot="icon">link</span></Button>
  </ToolbarGroup>
  <Divider />
  <Button variant="filled" toolbar-overflow="never" onClick={publish}>Publish</Button>
</Toolbar>

<Toolbar variant="docked" label="Message actions">
  <Button variant="text" label="Archive" onClick={archive}><span slot="icon">archive</span></Button>
  <Button variant="filled" onClick={reply}>Reply</Button>
</Toolbar>
```

**SwiftUI**

```swift
import Tanrei

// A docked toolbar sits in the bottom safe-area inset; its background reaches under it.
MessageView()
    .safeAreaInset(edge: .bottom, spacing: 0) {
        TanreiToolbar("Message actions") {
            TanreiIconButton("Archive", action: archive) { Image(systemName: "archivebox") }
            TanreiIconButton("Delete", action: delete) { Image(systemName: "trash") }
            TanreiButton("Reply", action: reply)
        }
    }

TanreiToolbar("Formatting", variant: .floating) {
    TanreiIconButton("Bold", isSelected: $bold) { Image(systemName: "bold") }
    TanreiIconButton("Insert link", action: insertLink) { Image(systemName: "link") }
}
```

**Compose**

```kotlin
import androidx.compose.foundation.gestures.Orientation
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.components.TanreiToolbar
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiToolbarVariant

// A docked toolbar pads its items by WindowInsets.navigationBars unless you pass windowInsets.
TanreiToolbar(contentDescription = "Message actions") {
    TanreiIconButton(onClick = ::archive, contentDescription = "Archive") { TanreiText("archive") }
    TanreiIconButton(onClick = ::delete, contentDescription = "Delete") { TanreiText("delete") }
    TanreiButton(text = "Reply", onClick = ::reply)
}

TanreiToolbar(contentDescription = "Formatting", variant = TanreiToolbarVariant.Floating) {
    TanreiIconButton(onClick = {}, contentDescription = "Bold", checked = bold, onCheckedChange = { bold = it }) {
        TanreiText("format_bold")
    }
    TanreiIconButton(onClick = ::insertLink, contentDescription = "Insert link") { TanreiText("link") }
}

// Vertical is for a floating toolbar; your layout places it.
TanreiToolbar(
    contentDescription = "Map controls",
    variant = TanreiToolbarVariant.Floating,
    orientation = Orientation.Vertical,
) {
    TanreiIconButton(onClick = ::zoomIn, contentDescription = "Zoom in") { TanreiText("add") }
    TanreiIconButton(onClick = ::zoomOut, contentDescription = "Zoom out") { TanreiText("remove") }
}
```

**Flutter**

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

TanreiToolbar(
  semanticLabel: 'Message actions',
  children: <Widget>[
    TanreiIconButton(onPressed: archive, semanticLabel: 'Archive', icon: const Icon(AppSymbols.archive)),
    TanreiIconButton(onPressed: delete, semanticLabel: 'Delete', icon: const Icon(AppSymbols.delete)),
    TanreiButton(onPressed: reply, child: const Text('Reply')),
  ],
);

TanreiToolbar(
  semanticLabel: 'Map controls',
  variant: TanreiToolbarVariant.floating,
  orientation: Axis.vertical,
  children: <Widget>[
    TanreiIconButton(onPressed: zoomIn, semanticLabel: 'Zoom in', icon: const Icon(AppSymbols.add)),
    TanreiIconButton(onPressed: zoomOut, semanticLabel: 'Zoom out', icon: const Icon(AppSymbols.remove)),
  ],
);
```

**React Native**

```tsx
import { Button, IconButton, Toolbar } from '@tanrei/react-native';

// The bottom and side insets come from TanreiProvider's insets; the toolbar's own insets prop replaces the sides it names.
<Toolbar accessibilityLabel="Message actions">
  <IconButton accessibilityLabel="Archive" onPress={archive}>archive</IconButton>
  <IconButton accessibilityLabel="Delete" onPress={remove}>delete</IconButton>
  <Button variant="filled" onPress={reply}>Reply</Button>
</Toolbar>

<Toolbar variant="floating" accessibilityLabel="Formatting">
  <IconButton accessibilityLabel="Bold" toggle selected={bold} onSelectedChange={setBold}>format_bold</IconButton>
  <IconButton accessibilityLabel="Insert link" onPress={insertLink}>link</IconButton>
</Toolbar>
```

## Specs

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

### Availability

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

### Platforms

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

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-toolbar` | lagging |
| SwiftUI | `TanreiToolbar` | lagging |
| Jetpack Compose | `TanreiToolbar` | lagging |
| Flutter | `TanreiToolbar` | lagging |
| React Native | `Toolbar` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `overflowLabel` (prop) | `overflowLabel` | `overflowLabel` | `overflowContentDescription` | `overflowSemanticLabel` | `overflowAccessibilityLabel` |

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 surface that holds the items: none of its own in the default system where the toolbar stands inline in the content; a bar across the bottom edge of the screen whose color reaches under the bottom system inset (docked); or a rounded, raised surface over the content (floating).
- `items`: The controls, in reading order: the product's own Tanrei Buttons, icon-only and labelled, toggles among them, Buttons that open a Menu, and Buttons with menu, alone or in groups. The toolbar lays them out and moves keyboard focus among them; it does not restyle them.
- `separator` (optional): A line across the toolbar between two groups, or where a separator item stands, or the empty space that takes its place, as the design system's separation says.
- `overflow-button` (optional): The icon-only Button at the end that opens a Menu of the items that do not fit, drawn only while some do not.
- `outline`: The docked toolbar's top edge, or the edge around a floating or an inline toolbar's whole shape (none in the default system for the last two).

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `inline`, `docked`, `floating` | `"inline"` | `variant` | Inline stands in place where the layout puts it, as long as its items need and no longer than the layout allows, with no surface, shadow or edge of its own in the default system (an editor's formatting bar, a table's actions, a row of filters); docked is a full-width bar along the bottom edge of the screen, flush with it; floating is a rounded, raised surface over the content, as long as its items. One product places a toolbar inside its content on one screen, docks it on another and floats it on a third. A design system may add values. |
| `orientation` | `enum`: `horizontal`, `vertical` | `"horizontal"` | `orientation` | The direction the items run. Vertical is for an inline or a floating toolbar beside the content (a floating one on a large screen); a docked toolbar is always horizontal. |
| `label` | `string` | `""` | `label` | Names the toolbar for assistive technology ("Formatting", "Message actions"). Never drawn. Required. |
| `overflow` | `enum`: `menu`, `scroll` | `"menu"` | `overflow` | What happens when the items do not fit along the toolbar's axis: menu hides items from the end into an overflow menu at the end of the toolbar (TBR-18); scroll keeps every item and scrolls them (TBR-8). |
| `overflowLabel` | `string` | `""` | `overflow-label` | The overflow button's accessible name ("More actions"), in the product's language. Never drawn. Empty takes the product's words. Needed whenever the toolbar may be narrower than its items. |
| `separation` | `enum`: `auto`, `divider`, `gap` | `"auto"` | `separation` | How two groups, or a separator item, set items apart: divider draws a line across the toolbar with space on each side, gap leaves empty space and no line. Auto takes comp.toolbar.separation, the design system's answer (divider in the default system). |

### Slots

| Slot | Description |
| --- | --- |
| `default` | Holds the items, in reading order: Tanrei Buttons, icon-only and labelled, Buttons with a Tanrei Menu anchored to them (the Menu beside its Button), Buttons with menu, groups of them and separators. |

### Item: `action`

An icon action: a Button of the text variant and the medium size with an icon and no label, unless the product sets otherwise, laid out by the toolbar and not restyled; a toggle (bold, mute) is one with toggle set.

It is a Button.

Names: Web `tanrei-button`, SwiftUI `TanreiButton`, Jetpack Compose `TanreiButton`, Flutter `TanreiButton`, React Native `Button`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | Its accessible name ("Bold"), as the Button's label. Required. |
| `variant` | `enum`: `filled`, `tonal`, `elevated`, `outlined`, `text` | `"text"` | `variant` | As the Button's variant: text for most actions, filled for the one the toolbar leads to (Done, Send). |
| `toggle` | `boolean` | `false` | `toggle` | As the Button's toggle: the action switches something on and off. |
| `selected` | `boolean` | `false` | `selected` | As the Button's selected: whether a toggle is on. |
| `disabled` | `boolean` | `false` | `disabled` | As the Button's disabled. |
| `overflow` | `enum`: `auto`, `never` | `"auto"` | — | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |

| Event | Description |
| --- | --- |
| `click` | The action was activated, as the Button's click. |
| `change` | A toggle was switched, as the Button's change. |

| Slot | Description |
| --- | --- |
| `icon` | Its glyph, as the Button's icon slot. A fixture item's content is this glyph. |

### Item: `button`

A labelled action: the button component, medium unless the product sets otherwise, laid out by the toolbar and not restyled.

It is a Button.

Names: Web `tanrei-button`, SwiftUI `TanreiButton`, Jetpack Compose `TanreiButton`, Flutter `TanreiButton`, React Native `Button`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `filled`, `tonal`, `elevated`, `outlined`, `text` | `"filled"` | `variant` | As the button's variant. |
| `toggle` | `boolean` | `false` | `toggle` | As the button's toggle: a labelled toggle (a filter that is on or off). |
| `selected` | `boolean` | `false` | `selected` | As the button's selected: whether a toggle is on. |
| `disabled` | `boolean` | `false` | `disabled` | As the button's disabled. |
| `overflow` | `enum`: `auto`, `never` | `"auto"` | — | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |

| Event | Description |
| --- | --- |
| `click` | The button was activated, as the button's click. |
| `change` | A toggle was switched, as the button's change. |

| Slot | Description |
| --- | --- |
| `default` | Its label, as the button's default slot. A fixture item's content is this label. |

### Item: `menu-button`

A Button that opens a Tanrei Menu anchored to it (Sort, Insert): icon-only unless the product sets otherwise, laid out by the toolbar as any other item and not restyled. The Menu is the product's, beside it in the toolbar; it is neither laid out nor an item.

It is a Button.

Names: Web `tanrei-button`, SwiftUI `TanreiButton`, Jetpack Compose `TanreiButton`, Flutter `TanreiButton`, React Native `Button`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | Its accessible name ("Sort"), as the Button's label. Required. |
| `variant` | `enum`: `filled`, `tonal`, `elevated`, `outlined`, `text` | `"text"` | `variant` | As the Button's variant. |
| `disabled` | `boolean` | `false` | `disabled` | As the Button's disabled. |
| `overflow` | `enum`: `auto`, `never` | `"auto"` | — | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |

| Event | Description |
| --- | --- |
| `click` | The button was activated, as the Button's click: the product opens its Menu. |

| Slot | Description |
| --- | --- |
| `icon` | Its glyph, as the Button's icon slot. A fixture item's content is this glyph. |

### Item: `button-with-menu`

A Button with menu: a main action and a menu of related ones, each half a stop of the toolbar's arrow keys (TBR-25), laid out by the toolbar and not restyled.

It is a Button with menu.

Names: Web `tanrei-button-with-menu`, SwiftUI `TanreiButtonWithMenu`, Jetpack Compose `TanreiButtonWithMenu`, Flutter `TanreiButtonWithMenu`, React Native `ButtonWithMenu`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | The leading half's label ("Save"), as the Button with menu's label. |
| `menuLabel` | `string` | `""` | `menu-label` | The trailing half's accessible name ("More save options"), as the Button with menu's menuLabel. |
| `variant` | `enum`: `filled`, `tonal`, `elevated`, `outlined` | `"filled"` | `variant` | As the Button with menu's variant. |
| `disabled` | `boolean` | `false` | `disabled` | As the Button with menu's disabled. |
| `overflow` | `enum`: `auto`, `never` | `"auto"` | — | Whether the toolbar may hide this item into its overflow menu when space runs out: never keeps it (Send, Done). Read by the toolbar to place the item. |

| Event | Description |
| --- | --- |
| `click` | The main action was activated, as the Button with menu's click. |
| `toggle` | The trailing half asks to open or close the menu, as the Button with menu's toggle. |

### Item: `group`

A run of related items (Bold, Italic, Underline), optionally named; the toolbar sets it apart from what is next to it as separation says. It draws nothing of its own.

Names: Web `tanrei-toolbar-group`, SwiftUI `TanreiToolbarGroup`, Jetpack Compose `TanreiToolbarGroup`, Flutter `TanreiToolbarGroup`, React Native `ToolbarGroup`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | Names the group for assistive technology ("Text style"); never drawn. Empty leaves the group unexposed: its items are the toolbar's. |

### Item: `separator`

A line across the toolbar between two items outside the groups: the toolbar draws it as it draws the line between two groups, and it is never a stop of the arrow keys.

It is a Divider.

Names: Web `tanrei-divider`, SwiftUI `TanreiDivider`, Jetpack Compose `TanreiDivider`, Flutter `TanreiDivider`, React Native `Divider`.

### States

`enabled`

### Accessibility

- Role: `toolbar`
- Labeling: label names the toolbar; it is never drawn. A group's label names the group; empty leaves it unexposed. Each item is named by its own label (an icon-only Button's label, a labelled Button's text), and the overflow button by overflowLabel or the product's words. The toolbar adds no name, role or state to its items.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus into the toolbar (to the item focused last, or the first enabled item) and out of it: the toolbar is one Tab stop. |
| Right Arrow / Left Arrow (horizontal), Down Arrow / Up Arrow (vertical) | Moves focus to the next or previous enabled item — a group's items and each half of a Button with menu included, separators skipped, the overflow button last — wrapping at the ends (Left and Right mirrored in right-to-left layouts). |
| Home / End | Moves focus to the first or last enabled item. |
| Enter or Space | Activates the focused item, as that Button does; on the overflow button, opens the menu of the hidden items. |

- The toolbar has no target of its own and takes no input: its items are the product's Buttons, icon-only and labelled, which meet the minimum target themselves (BTN-5). A medium item's target (48) fits inside a floating toolbar's 64, and reaches past an inline toolbar's 40 as a Button's does anywhere.
- The web has the toolbar role (with aria-orientation) and React Native has role toolbar. SwiftUI, Compose and Flutter have no toolbar role: there the toolbar is a group (an accessibility container, a traversal group, a semantics container) named by label, and each README says so.
- One Tab stop with arrow-key movement is the WAI-ARIA toolbar pattern. It uses the same roving focus the list, menu and tabs use on each platform; a Button outside a toolbar is unchanged.
- A toolbar holds Buttons, menu buttons, Buttons with menu, groups of them and separators. Anything else is laid out and reported (TBR-12), because a control that uses the arrow keys would take the keys the toolbar moves focus with.
- A docked toolbar and a navigation bar are not used on the same screen: both claim the bottom edge.
- How the items look is their own design system's answer; the toolbar reads no state tokens.
- Hidden items stay reachable: the overflow button is the last stop of the arrow keys, and its menu holds every hidden item with its own name and state; activating one is activating the item.

### Behaviour

Requirements in EARS form, each with its stable id.

- **TBR-1** WHILE variant is docked THE toolbar SHALL span the full width of its parent, be at least comp.toolbar.container.size.docked high above the bottom system inset, fill its container with comp.toolbar.container.color.docked, raise it by comp.toolbar.container.elevation.docked, round it by comp.toolbar.container.shape.docked, and draw its top edge comp.toolbar.outline.width thick in comp.toolbar.outline.color.docked.
- **TBR-2** WHILE variant is docked THE toolbar SHALL lay its items out in one row in reading order, the first comp.toolbar.container.padding-inline.docked from the start edge and the last as far from the end edge, with equal space between neighbours and never less than comp.toolbar.container.gap.docked, each item vertically centered in the row; a toolbar with one item SHALL center it.
- **TBR-3** WHILE variant is docked THE toolbar SHALL extend its container under the bottom system inset (home indicator, gesture bar, taskbar) and SHALL place its items above that inset and inside the left and right insets, so that its color reaches the screen's bottom edge while no item sits under a system bar.
- **TBR-4** WHILE variant is floating THE toolbar SHALL fill its container with comp.toolbar.container.color.floating, raise it by comp.toolbar.container.elevation.floating, round it by comp.toolbar.container.shape.floating eased by sys.shape.corner-smoothing, draw an edge comp.toolbar.outline.width thick in comp.toolbar.outline.color.floating around its whole shape, be as long along its axis as its items need — comp.toolbar.container.padding-inline.floating at each end and comp.toolbar.container.gap.floating between neighbours, or as much more as TBR-16 keeps for their targets — and at least comp.toolbar.container.size.floating across it, with every item centered across the axis.
- **TBR-5** WHILE variant is floating THE toolbar SHALL NOT position itself: it SHALL lay out at its own size where the layout that holds it places it (normally centered above the bottom edge of the content, clear of a navigation bar and of the system inset, or beside the content's end edge for a vertical toolbar), and SHALL NOT draw a scrim, move content or reach under a system inset.
- **TBR-6** WHILE orientation is vertical and variant is floating or inline THE toolbar SHALL stack its items from top to bottom in the order given, with its variant's paddings and gap along the vertical axis and at least its variant's container size as its width; WHILE orientation is vertical and variant is docked THE toolbar SHALL lay out as a horizontal docked toolbar and in debug builds report that a docked toolbar is horizontal.
- **TBR-7** WHEN the layout direction is right-to-left THE toolbar SHALL lay out a horizontal toolbar's items from the right edge, in reading order, mirrored; a vertical toolbar SHALL keep its top-to-bottom order.
- **TBR-8** WHILE overflow is scroll and the items need more length than the toolbar has along its axis THE toolbar SHALL keep every item at its own size and the gap at its minimum, SHALL scroll its items along its axis so that every item stays reachable, and SHALL bring an item that receives keyboard focus fully into view; it SHALL NOT wrap, shrink or hide an item.
- **TBR-9** THE toolbar SHALL NOT restyle its items: each SHALL render as the Button it is, from its own tokens, variant and size, and SHALL keep its own press, focus, target and disabled behaviour and its own semantics, a toggle Button staying a toggle. Between two items the hit areas SHALL never overlap (as BGR-4): in a docked toolbar they SHALL meet at the middle of the gap, and in a floating or an inline one where the lengths TBR-16 keeps for the two items meet; each item SHALL keep its full target on every side that has no neighbour. In a docked toolbar the gap (at least comp.toolbar.container.gap.docked) is what keeps an item's target whole between its neighbours: at sys.target.touch minus the item's length or more, every item keeps its full target there, as the default system's gap does for every Button size; a layer that narrows that gap narrows the targets between items. In a floating or an inline toolbar TBR-16 keeps it, whatever comp.toolbar.container.gap.floating or comp.toolbar.container.gap.inline is.
- **TBR-10** THE toolbar SHALL be one Tab stop: the item that had keyboard focus last, or else the first item that is not disabled. Its items are its own and its groups' items, in reading order; a separator and a Menu anchored to an item are none. WHILE an item has keyboard focus, Right Arrow and Left Arrow in a horizontal toolbar (Left Arrow moving to the next item in a right-to-left layout) and Down Arrow and Up Arrow in a vertical one SHALL move focus to the next and previous item that is not disabled, wrapping at the ends, and Home and End to the first and last such item; moving focus SHALL NOT activate an item, and Enter and Space SHALL act on the focused item as that item does. A disabled item SHALL NOT take focus, as its own spec says (BTN-2).
- **TBR-11** THE toolbar SHALL expose itself as a toolbar named by label, with its orientation, on a platform that has a toolbar role, and on any other platform as a group named by label that contains its items in order; each item SHALL keep its own name, role and state, and the toolbar SHALL add nothing to them.
- **TBR-12** WHILE label is empty THE toolbar SHALL still render, and in debug builds report that it has no accessible name. IF the toolbar holds no item, or an element that is neither one of its item kinds nor a Menu anchored to one of its items, THE toolbar SHALL still lay it out, and in debug builds report it, because a field or a slider would take the arrow keys the toolbar moves focus with (the WAI-ARIA toolbar pattern).
- **TBR-13** WHEN variant names a value that the active design system does not have, or orientation a value the component does not define, THE toolbar SHALL render with the default value (inline, horizontal), and in debug builds report the unknown value. A variant value that a layer adds SHALL take the inline structure with its own values, because the structure is not a value a layer can set. WHEN overflow or separation names a value the component does not define THE toolbar SHALL render with menu or auto, and in debug builds report it.
- **TBR-14** WHILE the text size is enlarged THE toolbar SHALL grow across its axis to hold its items (its size is a minimum) and SHALL NOT scale its items itself; each item grows or keeps its size as its own spec says.
- **TBR-15** THE toolbar SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the toolbar is shown THE toolbar SHALL draw itself for the new direction at once, without a motion of its own, keeping its focus and the item its Tab stop stands on.
- **TBR-16** WHILE variant is floating THE toolbar SHALL lay its items comp.toolbar.container.gap.floating apart (and WHILE it is inline comp.toolbar.container.gap.inline apart, TBR-17), measured between their visible bounds along its axis, and SHALL move two neighbours further apart only as far as each needs in order to keep, centred on itself, a length of sys.target.touch along the axis that its neighbour's does not reach into, because items that stand this close cannot extend a hit area over each other (as BGR-3): where the token is 0, as in the default system, an item at least sys.target.touch long touches its neighbour, and a shorter one stands half of what it lacks away from it. THE toolbar SHALL NOT widen, resize or restyle an item to do so (TBR-9), SHALL space its items the same whatever the input type, so that the toolbar does not change with the pointer, and SHALL space them so along the vertical axis WHILE orientation is vertical (TBR-6). A floating toolbar is as long as its items need (TBR-4) and never spreads them across a width; WHILE variant is docked THE toolbar SHALL spread its items across its width and keep comp.toolbar.container.gap.docked as the least space between them (TBR-2), unchanged.
- **TBR-17** WHILE variant is inline THE toolbar SHALL lay its items out in one line along its axis where the layout places it, as long as its items need and never longer than the layout allows, with comp.toolbar.container.padding-inline.inline at each end, comp.toolbar.container.gap.inline between neighbours spaced as TBR-16 spaces a floating toolbar's, and at least comp.toolbar.container.size.inline across its axis; it SHALL fill its container with comp.toolbar.container.color.inline, raise it by comp.toolbar.container.elevation.inline, round it by comp.toolbar.container.shape.inline and edge it comp.toolbar.outline.width thick in comp.toolbar.outline.color.inline (transparent, none and no edge in the default system), and SHALL NOT read a system inset, position itself or spread its items.
- **TBR-18** WHILE overflow is menu and the items, at their own sizes and with the least gaps, need more length along the axis than the layout offers the toolbar — the length it would take with every item shown, which a parent as long as its content passes on from further up, so that it can grow while the toolbar and its parent stay as long as the items still shown — THE toolbar SHALL hide items from its end, in reverse reading order and one at a time, skipping every item whose overflow is never, until the items that remain and the overflow button fit; SHALL draw the overflow button after the last item that remains — an icon-only Tanrei Button of the text variant and the medium size, showing the glyph more_horiz in a horizontal toolbar and more_vert in a vertical one, named by overflowLabel or the product's words, exposed as a button that opens a menu, with its expanded state; and SHALL NOT hide an item whose overflow is never even when the items then do not fit, in which case TBR-8 applies to what remains.
- **TBR-19** THE toolbar SHALL decide which items are hidden before the first frame in which it is drawn, and again in the frame in which its items or the length the layout offers it (TBR-18) change, so that no item is drawn and then hidden in a later frame, and its size across its axis never changes because an item was hidden or shown; hiding and showing items SHALL NOT be animated, because it answers a change of the layout that a motion would lag behind.
- **TBR-20** WHEN the overflow button is activated THE toolbar SHALL open a Tanrei Menu anchored to it, placed bottom-end (in a vertical toolbar, at the side away from the edge it stands at), holding the hidden items in reading order: an action or a labelled Button as a menu item with its label and its icon, disabled while it is; a toggle as a checkbox item with its selected state; a menu button as a submenu item holding its menu's items; a Button with menu as an item for its main action followed by a submenu item named by its menuLabel; and a separator, or the boundary between two groups, as the menu's separation. Activating a menu item SHALL do exactly what activating the hidden item does — the same click or change, once, from that item — and close the menu.
- **TBR-21** WHEN the item that is the toolbar's Tab stop, or has keyboard focus, is hidden THE toolbar SHALL make the overflow button the Tab stop and, if focus was on the item, move focus to the overflow button; THE overflow button SHALL be the last stop of the arrow-key order (TBR-10).
- **TBR-22** THE toolbar SHALL lay out a group's items in its own line, in place, as items of the toolbar (the group draws nothing of its own), SHALL expose a group whose label is not empty as a group named by that label, and a group whose label is empty as nothing (its items are the toolbar's), unless the product names it (TBR-26).
- **TBR-23** THE toolbar SHALL set two neighbouring groups — or a group and an item outside any group — apart as separation names: WHILE it is divider by a line comp.toolbar.divider.thickness thick and comp.toolbar.divider.length long across the axis, centred, in comp.toolbar.divider.color, with comp.toolbar.group.gap on each side; WHILE it is gap by twice comp.toolbar.group.gap of empty space. A separator item SHALL be drawn the same way wherever it stands. THE toolbar SHALL expose each such line as a separator, SHALL never make it a stop of the arrow keys, and SHALL draw no line at the start or the end of the toolbar, and never two lines side by side.
- **TBR-24** WHILE an item is the anchor of a Menu THE toolbar SHALL keep it an item of the arrow-key order and SHALL neither lay out nor count the Menu; WHILE that Menu is open its keys SHALL be the Menu's, and WHEN it closes focus SHALL return to its anchor, which stays the Tab stop.
- **TBR-25** THE toolbar SHALL make each half of a Button with menu a stop of its own in the arrow-key order, the leading half first in reading order, each keeping its own name, role and state.
- **TBR-26** WHERE the platform has ARIA attributes (the web), the toolbar's host and a group's host carries its role (TBR-11, TBR-22) itself, so what the product writes on it is not moved, but for a name: WHILE label is not empty THE toolbar SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is its whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE label is empty a name the product gives so — aria-label, or aria-labelledby naming an element — SHALL be its name and SHALL NOT be reported as having none in debug builds (TBR-12). A group whose label is empty and that the product names so SHALL be exposed as a group named by that name, as one with a label is: a group the product names is a named group, and a name on an element with no role would name nothing (TBR-22). WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaLabelledByElements) to null, THE toolbar SHALL take it away.

### 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.toolbar.…` → `--tanrei-comp-toolbar-…`).

#### `tanrei.comp.toolbar`

| Token | Type | Value |
| --- | --- | --- |
| `comp.toolbar.container.color.inline` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.toolbar.container.color.docked` | `color` | `{tanrei.sys.color.surface}` |
| `comp.toolbar.container.color.floating` | `color` | `{tanrei.sys.color.surface-bright}` |
| `comp.toolbar.container.elevation.inline` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.toolbar.container.elevation.docked` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.toolbar.container.elevation.floating` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.toolbar.container.shape.inline` | `dimension` | `{tanrei.sys.shape.corner.medium}` |
| `comp.toolbar.container.shape.docked` | `dimension` | `{tanrei.sys.shape.corner.none}` |
| `comp.toolbar.container.shape.floating` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.toolbar.container.size.inline` | `dimension` | `40px` |
| `comp.toolbar.container.size.docked` | `dimension` | `64px` |
| `comp.toolbar.container.size.floating` | `dimension` | `64px` |
| `comp.toolbar.container.padding-inline.inline` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.toolbar.container.padding-inline.docked` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.toolbar.container.padding-inline.floating` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.toolbar.container.gap.inline` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.toolbar.container.gap.docked` | `dimension` | `16px` |
| `comp.toolbar.container.gap.floating` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.toolbar.outline.color.inline` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.toolbar.outline.color.docked` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.toolbar.outline.color.floating` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.toolbar.outline.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.toolbar.group.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.toolbar.divider.length` | `dimension` | `{tanrei.sys.space.space300}` |
| `comp.toolbar.divider.thickness` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.toolbar.divider.color` | `color` | `{tanrei.sys.color.outline-variant}` |
| `comp.toolbar.separation` | `number` | `{tanrei.sys.behavior.separation}` |

## Guidelines

### When to use

Use a toolbar for a set of controls that act on what is in front of the person: Bold, Italic and Link above an editor; Filter, Sort and Download above a table; Archive, Delete and Move for an open message; a pen and an eraser over a drawing; zoom and location on a map. The controls act on the current content or selection, not on where the person goes next.

For the screen’s title, the way back and one or two global actions, use the [app bar](https://tanrei.chnotchy.com/components/app-bar/) at the top. To move between the top-level destinations of an app, use the [navigation bar](https://tanrei.chnotchy.com/components/navigation-bar/). A docked toolbar and a navigation bar never share a screen: both claim the bottom edge.

### Inline, docked or floating

| Variant | Where it sits | Items | Use for |
| --- | --- | --- | --- |
| Inline (default) | In place in the content, where your layout puts it, with no surface of its own in the default system | Closed up, as long as they need, each keeping room for its own touch target | The controls of a part of the page: an editor’s formatting, a table’s actions, a row of filters |
| Docked | Across the bottom edge, flush with it, its color reaching under the system bar | Spread across the width, with equal space between | The actions of a detail screen on a phone: an open message, a photo, a document |
| Floating | A rounded, raised surface over the content, placed by your layout | Closed up, as long as they need, each keeping room for its own touch target | Tools over content that should stay visible around them: a canvas, a map |

A docked toolbar is always horizontal. An inline or a floating one may be vertical, beside the content. One product can place a toolbar inline on one screen, dock one on another and float one on a third. A variant that a design system adds takes the inline structure with its own values.

A docked toolbar spreads its items across the width and never brings them closer than its gap (16 in the default system). An inline or a floating toolbar closes its items up: two neighbours stand apart only as far as each needs to keep a touch target of its own along the row, and never closer than the gap (4 inline and 0 floating in the default system). Large buttons touch, medium ones end up 8 apart and small ones 16. The toolbar never widens or restyles an item to do this, and the spacing is the same for a mouse and for touch.

### Items

- Use icon-only [Buttons](https://tanrei.chnotchy.com/components/button/) of the `text` variant for most actions. Give the one action the toolbar leads to, such as Send or Done, more weight: a filled icon-only button, perhaps at the `wide` width, or a filled button with a label when a word reads better than an icon.
- A `narrow` icon-only button suits a toolbar with many small actions. It is drawn narrower than its target, and the toolbar keeps room for the target between its neighbours.
- Make an action that switches something on and off (Bold, Mute) an icon-only button with `toggle`. A filter that is on or off is a labelled button with `toggle`. Each keeps its own selected state and its own announcement.
- For a control that offers a choice of several commands, anchor a [menu](https://tanrei.chnotchy.com/components/menu/) to a Button in the toolbar. For a main action with variations, such as Save and Save as, use a [button with menu](https://tanrei.chnotchy.com/components/button-with-menu/).
- Keep the order stable. People find actions by position, and a toolbar whose items move between screens is hard to learn.
- A docked toolbar with one item centers it. Consider whether that item belongs in the content instead.

### Groups and separators

Put related items in a group: Bold, Italic and Underline; Undo and Redo; Zoom in and Zoom out. The toolbar sets two groups apart, and a group from an item outside any group, the way the design system sets parts apart elsewhere: in the default system with a short line and 8 of space on each side, or, under a design system whose separation is space, with 16 of empty space and no line. Set `separation` on a toolbar only for a reason of its own. A separator item draws the same line between two items that are not in groups.

The toolbar never draws a line at either end, and never two side by side. Give a group a `label` when the grouping means something to the person (“Text style”), so that a screen reader names it. Without a label, the group sets its items apart and says nothing.

### When the items do not fit

The toolbar never wraps or shrinks an item. When its items need more room than it has, it hides items from the end, one at a time, and shows an overflow button after the last item that remains. The button opens a menu of the hidden items, in their order, with their labels, icons and states: an action is a menu item, a toggle a checkbox item, a menu button a submenu, and a button with menu its main action followed by a submenu. Choosing one does exactly what the hidden item does. Name the overflow button with `overflow-label` (“More actions”), or once for the product with `setWords({ toolbarOverflowLabel: … })`.

Mark the item that must stay, such as Send or Done, with `toolbar-overflow="never"`: the toolbar hides the items before it instead. If what is never hidden still does not fit, the rest of the row scrolls. To keep every item in the row and scroll them instead of hiding any, set `overflow="scroll"`.

What is hidden is decided before the toolbar is drawn, and again in the frame in which its length changes, so an item is never drawn and then hidden, and the row never jumps.

### Do

- Name the toolbar for what its controls do: “Formatting”, “Message actions”.
- Disable an action that cannot apply right now rather than removing it, so the others do not move.
- Order the items so that the ones people need most come first: the toolbar hides from the end.
- Place a floating toolbar clear of the content people need to see while they use it, and clear of a navigation bar and the system inset.

### Don’t

- Don’t put a text field, a slider or a set of chips in a toolbar. Each one needs the arrow keys that the toolbar uses to move focus. Search belongs in the app bar or the page.
- Don’t use a docked toolbar and a navigation bar on the same screen.
- Don’t restyle the items to make the toolbar look emphasized. Choose the tonal or filled variant for the buttons instead.
- Don’t put navigation destinations in a toolbar. Its items are controls that act on the content.

## Accessibility

### What the component handles

- The toolbar is one Tab stop, following the WAI-ARIA toolbar pattern. Tab enters it at the item that had keyboard focus last, or else at the first item that is not disabled, and the next Tab leaves it. A formatting toolbar of eight toggles costs one Tab press to cross, not eight.
- Inside, the arrow keys along its axis move to the next or previous item and wrap at the ends: Right and Left Arrow in a horizontal toolbar, Down and Up Arrow in a vertical one. Home and End move to the first and last item. In a right-to-left layout, Left Arrow moves to the next item. A group’s items are the toolbar’s items, each half of a button with menu is a stop of its own, separators are skipped, and the overflow button is the last stop.
- Moving focus never activates an item. Enter and Space act on the focused item as that item does, so a toggle still switches.
- While a menu anchored to an item is open, the keys are the menu’s. When it closes, focus returns to the item it opened from.
- When the item that has focus is hidden because the toolbar got narrower, focus moves to the overflow button.
- Disabled items are skipped by the arrow keys and are never the Tab stop, because a disabled button is not focusable.
- The roving focus is the same one the list, menu and tabs use on each platform.
- The toolbar is exposed as a toolbar named by `label`, with its orientation, where the platform has a toolbar role (the web, React Native). SwiftUI, Compose and Flutter have none: there it is a group named by `label` that contains its items in order.
- A group with a `label` is exposed as a group with that name. Lines between groups are exposed as separators.
- The overflow button is exposed as a button that opens a menu, with its expanded state, and its menu holds every hidden item with its own name and state.
- Each item keeps its own name, role, state and target. The toolbar adds nothing to them and never restyles them.
- Between two items the hit areas never overlap. In a docked toolbar they meet at the middle of the gap, and the default system’s gap (16) is wide enough that every item keeps its whole 48 touch target there too; a design system with a narrower gap narrows the targets between items. In an inline or a floating toolbar the items stand just far enough apart for each to keep a touch target’s length of its own along the row, whatever the pointer, so a narrow or a small button is never squeezed by its neighbours. On a side with no neighbour, an item keeps its whole target.
- When `overflow` is `scroll` and an item that receives keyboard focus is scrolled out of view, the toolbar scrolls it fully into view.
- At larger text sizes the toolbar grows across its axis to hold its items. It does not scale them.
- Debug builds report a missing `label`, a toolbar with no items, an item that is none of its kinds, a docked toolbar set to vertical, a missing name for the overflow button, and an unknown variant, orientation, overflow or separation.

Known platform gaps:

- SwiftUI, Compose and Flutter have no toolbar role. A screen reader hears a named group, not a toolbar, and each package’s README says so. The group does not expose its orientation.
- SwiftUI, Compose, Flutter and React Native are lagging: they have no inline variant, no groups or separators, no overflow menu, and they take only Buttons as items.
- Web: a docked toolbar reads `env(safe-area-inset-bottom)`, which is zero unless the page’s viewport meta has `viewport-fit=cover`.
- Web: the overflow menu of a vertical toolbar opens below the overflow button, not beside it, because the menu has no side placement.
- Web: an inline or a floating toolbar measures the room it may take from its parent element. A toolbar whose room is set by its siblings in a flex row, not by its parent, decides what fits only when its parent or its items change size.
- Web: while the items scroll, whatever of an item reaches past the toolbar across its axis is clipped: its hit area, its focus ring or glow, an elevated button’s shadow. No scrollbar is shown.
- On SwiftUI the one Tab stop is a focus group for the keyboard: Full Keyboard Access moves among its items with the arrow keys and leaves it with Tab. The keys are tested as rules, not yet with a real keyboard.
- SwiftUI: while the items scroll, an end item’s hit area stops at the toolbar’s edge. When a resize makes the items start or stop scrolling, they are recreated, so an item loses its keyboard focus or a press in progress.
- On React Native the Tab stop and the arrow keys work only where a hardware keyboard is attached, as in the tabs row.
- React Native has the toolbar role but no way to expose an orientation, so a vertical toolbar is not announced as vertical.
- React Native: the floating toolbar’s width, the keys on a hardware keyboard, scrolling a focused item into view, and whether VoiceOver and TalkBack announce the toolbar have been checked in Jest only, not on a device.
- Under `m3-like`, the toolbar draws the default system’s values: Material’s toolbar values wait for the fidelity reference to cover it. Material’s vibrant toolbar, which recolors its items, cannot be drawn, because the toolbar never restyles its items (mimicry gap `toolbar-vibrant-color`).

### What you need to do

- Give the toolbar a `label`, every item its own label in your product’s language, including icon-only actions, and the overflow button a name with `overflow-label` or your product’s words.
- Put only Tanrei Buttons and buttons with menu in it, alone, in groups, or with menus anchored to them.
- When an action cannot apply, disable it, and say why nearby when the reason is not obvious.
- Open and close the menu of a menu button yourself, as you do for any menu.
- Place a floating toolbar so that it does not cover content that is still needed while the toolbar is open.
