Skip to content
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Getting startedWebReactSwiftUIJetpack ComposeFlutterReact NativePresetsYour own design systemUsing Tanrei with AI agentsBrandDesign tokensDesign systems and customizationTheme StudioColorTypographyIconsShapeElevationMotionInteraction statesBehaviourLayoutAccessibilityWordsColor tokensTypescale tokensShape tokensElevation tokensMotion tokensState tokensSpace and measure tokensComponentsButtonMain action buttonSegmented buttonButton groupButton with menuToolbarCopy buttonCheckboxRadio buttonSwitchChipMenuSliderSlider fieldCalendarDateTimePickerField groupRadio groupCheckbox groupSelectSelect fieldMenubarFile uploadFile picker AdvancedDrop zoneText input AdvancedText fieldChip fieldFieldCombobox AdvancedCombobox fieldNumber input AdvancedNumber fieldColor input AdvancedColor fieldOne-time-code input AdvancedOne-time-code fieldEmbedded text input AdvancedChat composerCardDividerModal layer AdvancedDialogListList rowBottom sheetSide sheetCarouselCode blockTablePopoverStepsAccordionDescription listFigureApp shellPage headerTree viewTimelineImage previewHandleResizable frameScroll areaBadgeProgress indicatorMeterNumber tickerLoading indicatorSnackbarTooltipKeyboard shortcutTagInline codeCalloutAvatarEmpty stateTag groupSkeletonFile itemSwatchMarquee textTabsTab viewApp barNavigation barNavigation drawerSearchLinkPaginationPrevious and next linksStepperBreadcrumbsTable of contentsLink cardFAB menuStatusHow Tanrei is builtContributingLicense and attribution
HomeComponentsToolbar

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.

content_copyView as Markdown
Category
Actions
Tokens
comp.toolbar
Level
Base

Overview

Web
tanrei-toolbar
schedule
SwiftUI
TanreiToolbar
schedule
Jetpack Compose
TanreiToolbar
schedule
Flutter
TanreiToolbar
schedule
React Native
Toolbar
schedule

Generated from docs/status/status.json, specs/toolbar.spec.json

Inline, docked and floating

format_boldformat_italicformat_underlinedlinkimagearchivedeletemark_email_unreaddrive_file_moveeditink_highlighterink_erasercheck

Usage

A toolbar holds a set of related controls: actions, toggles, a button that opens a menu, a button with menu. Its items are your own Tanrei Buttons 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 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, 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 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.

<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>
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>
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") }
}
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") }
}
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)),
  ],
);
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

Groups and a separator, a menu button, a button with menu, labelled toggles, an overflow menu

undoredosortOpenAssigned to meHas attachmentsarchivedeletemark_email_unreaddrive_file_movelabelflagsnoozeprintattach_fileimagelinkSendaddremovemy_locationlayers

Anatomy

Part Description
1 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).
2 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.
3 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.
4 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.
5 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).

Properties

Property Type Default Description
variant inline | docked | floating "inline" 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 horizontal | vertical "horizontal" 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 "" Names the toolbar for assistive technology ("Formatting", "Message actions"). Never drawn. Required.
overflow menu | scroll "menu" 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 "" 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 auto | divider | gap "auto" 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.

Items

The kinds of item the component repeats. A fixture lists its items in order; each platform names each kind in its own idiom.

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.

Property Type Default Description
label string "" Its accessible name ("Bold"), as the Button's label. Required.
variant filled | tonal | elevated | outlined | text "text" As the Button's variant: text for most actions, filled for the one the toolbar leads to (Done, Send).
toggle boolean false As the Button's toggle: the action switches something on and off.
selected boolean false As the Button's selected: whether a toggle is on.
disabled boolean false As the Button's disabled.
overflow 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.
Slot Description
icon Its glyph, as the Button's icon slot. A fixture item's content is this glyph.
Event Description
click The action was activated, as the Button's click.
change A toggle was switched, as the Button's change.
Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-button TanreiButton TanreiButton TanreiButton Button
label · Property label label contentDescription semanticLabel accessibilityLabel
variant · Property variant variant variant variant variant
toggle · Property toggle isSelected onCheckedChange isSelected toggle
selected · Property selected isSelected checked isSelected selected
disabled · Property disabled disabled enabled onPressed disabled
overflow · Property toolbar-overflow toolbarOverflow Modifier.toolbarOverflow TanreiToolbarItem.overflow toolbarOverflow
click · Event click action onClick onPressed onPress
change · Event change isSelected onCheckedChange onPressed onSelectedChange
Notes An icon-only <tanrei-button> child; the glyph is its content. An icon-only TanreiButton in the toolbar's view builder; a toggle is its isSelected: initializer. An icon-only TanreiButton called in the toolbar's content lambda. An icon-only TanreiButton in the toolbar's children. An icon-only Button element among the toolbar's children.

button

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

Property Type Default Description
variant filled | tonal | elevated | outlined | text "filled" As the button's variant.
toggle boolean false As the button's toggle: a labelled toggle (a filter that is on or off).
selected boolean false As the button's selected: whether a toggle is on.
disabled boolean false As the button's disabled.
overflow 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.
Slot Description
default Its label, as the button's default slot. A fixture item's content is this label.
Event Description
click The button was activated, as the button's click.
change A toggle was switched, as the button's change.
Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-button TanreiButton TanreiButton TanreiButton Button
variant · Property variant variant variant variant variant
toggle · Property toggle isSelected onCheckedChange isSelected toggle
selected · Property selected isSelected checked isSelected selected
disabled · Property disabled disabled enabled onPressed disabled
overflow · Property toolbar-overflow toolbarOverflow Modifier.toolbarOverflow TanreiToolbarItem.overflow toolbarOverflow
click · Event click action onClick onPressed onPress
change · Event change isSelected onCheckedChange onPressed onSelectedChange
Notes A <tanrei-button> child; the label is its content. A TanreiButton in the toolbar's view builder. A TanreiButton called in the toolbar's content lambda. A TanreiButton in the toolbar's children. A Button element among the toolbar's children.

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.

Property Type Default Description
label string "" Its accessible name ("Sort"), as the Button's label. Required.
variant filled | tonal | elevated | outlined | text "text" As the Button's variant.
disabled boolean false As the Button's disabled.
overflow 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.
Slot Description
icon Its glyph, as the Button's icon slot. A fixture item's content is this glyph.
Event Description
click The button was activated, as the Button's click: the product opens its Menu.
Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-button TanreiButton TanreiButton TanreiButton Button
label · Property label label contentDescription semanticLabel accessibilityLabel
variant · Property variant variant variant variant variant
disabled · Property disabled disabled enabled onPressed disabled
overflow · Property toolbar-overflow toolbarOverflow Modifier.toolbarOverflow TanreiToolbarItem.overflow toolbarOverflow
click · Event click action onClick onPressed onPress
Notes A <tanrei-button id> child with a <tanrei-menu anchor="<its id>"> beside it, in the toolbar or in the same group. A TanreiButton with .tanreiMenu { … } attached, in the toolbar's view builder. A TanreiButton with a TanreiMenu anchored to it, called in the toolbar's content lambda. A TanreiButton that opens a TanreiMenu anchored to it, in the toolbar's children. A Button with a Menu anchored to it, among the toolbar's children.

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.

Property Type Default Description
label string "" The leading half's label ("Save"), as the Button with menu's label.
menuLabel string "" The trailing half's accessible name ("More save options"), as the Button with menu's menuLabel.
variant filled | tonal | elevated | outlined "filled" As the Button with menu's variant.
disabled boolean false As the Button with menu's disabled.
overflow 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.
Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-button-with-menu TanreiButtonWithMenu TanreiButtonWithMenu TanreiButtonWithMenu ButtonWithMenu
label · Property label title text label label
menuLabel · Property menuLabel menuLabel menuContentDescription menuSemanticLabel menuAccessibilityLabel
variant · Property variant variant variant variant variant
disabled · Property disabled disabled enabled onPressed disabled
overflow · Property toolbar-overflow toolbarOverflow Modifier.toolbarOverflow TanreiToolbarItem.overflow toolbarOverflow
click · Event click action onClick onPressed onPress
toggle · Event toggle isMenuPresented onMenuExpandedChange menuController onToggle
Notes A <tanrei-button-with-menu> child, its menu items in slot="menu" as anywhere. A TanreiButtonWithMenu in the toolbar's view builder. A TanreiButtonWithMenu called in the toolbar's content lambda. A TanreiButtonWithMenu in the toolbar's children. A ButtonWithMenu among the toolbar's children.

group · group — holds items

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.

Property Type Default Description
label string "" Names the group for assistive technology ("Text style"); never drawn. Empty leaves the group unexposed: its items are the toolbar's.
Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-toolbar-group TanreiToolbarGroup TanreiToolbarGroup TanreiToolbarGroup ToolbarGroup
label · Property label label contentDescription semanticLabel accessibilityLabel
Notes A <tanrei-toolbar-group> child holding items: display: contents, with role="group" and aria-label while its label is not empty. TanreiToolbarGroup(_ label:) { items } in the toolbar's view builder: an accessibility container named by its label, or none without one. TanreiToolbarGroup(contentDescription) { … } in the toolbar's content lambda: a traversal group named by its description, or none without one. TanreiToolbarGroup(semanticLabel:, children:) in the toolbar's children: a semantics container named by its label, or none without one. <ToolbarGroup accessibilityLabel>{items}</ToolbarGroup> among the toolbar's children: a view with role group while it has a label.

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.

Spec Web SwiftUI Jetpack Compose Flutter React Native
Item tanrei-divider TanreiDivider TanreiDivider TanreiDivider Divider
Notes A <tanrei-divider> child: the toolbar draws the line itself, as between two groups (TBR-23), as an element of its own with role="separator", and leaves the divider unassigned. A TanreiDivider() in the toolbar's view builder; the toolbar draws the line from its own tokens. A TanreiDivider() called in the toolbar's content lambda; the toolbar draws the line from its own tokens. A TanreiDivider() in the toolbar's children; the toolbar draws the line from its own tokens. A Divider among the toolbar's children; the toolbar draws the line from its own tokens.

States

Behavior

Requirements in EARS form. Every platform implementation is tested against these ids.

ID Requirement
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.

Platform name mapping

Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.

Spec Web SwiftUI Jetpack Compose Flutter React Native
Component tanrei-toolbar TanreiToolbar TanreiToolbar TanreiToolbar Toolbar
variant · Property variant variant variant variant variant
orientation · Property orientation orientation orientation orientation orientation
label · Property label label contentDescription semanticLabel accessibilityLabel
overflow · Property overflow overflow overflow overflow overflow
overflowLabel · Property overflowLabel overflowLabel overflowContentDescription overflowSemanticLabel overflowAccessibilityLabel
separation · Property separation separation separation separation separation
Notes The items are <tanrei-button> children, icon-only and labelled, <tanrei-button-with-menu> children, <tanrei-toolbar-group> children holding them, and <tanrei-divider> separators; a <tanrei-menu anchor> child anchored to an item's id is that item's Menu and is not laid out. The host has role="toolbar", aria-orientation and aria-label from label; a group with a label has role="group" and aria-label. The toolbar assigns each of its children, and a group each of its items, to a slot of its own (manual slot assignment), so that the toolbar draws the separations between them and leaves a hidden item unassigned: a hidden item is not drawn and keeps its place, its listeners and its state in the page. The overflow button and its menu are elements of the toolbar's shadow root. The roving Tab stop sets tabindex on the item elements (0 on the Tab stop, -1 on the others): an item delegates focus to the button in its shadow root, and a host whose tabindex is -1 takes that button out of the Tab order; a Button with menu is told which of its halves is the Tab stop through the internal channel (SPB-33). A docked toolbar's bottom inset is env(safe-area-inset-bottom). Snapshot harness: the stages of TBR-1, TBR-4, TBR-6 and TBR-18. TanreiToolbar(_ label: String, variant:, orientation:) { items }, a view builder of TanreiButtons, icon-only and labelled. A docked toolbar is placed with .safeAreaInset(edge: .bottom), its background ignoring the bottom safe area. SwiftUI's own .toolbar with a bottom-bar placement is not used: its bar cannot take the tokens. SwiftUI has no toolbar role: the toolbar is an accessibility container (.accessibilityElement(children: .contain)) labelled by label. The roving Tab stop is one focus group for the keyboard: Full Keyboard Access moves among its items with the arrow keys and leaves it with Tab. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. .toolbarOverflow(.never) on an item keeps it; the overflow menu is a SwiftUI Menu drawn from the Tanrei Menu's tokens. TanreiToolbar(contentDescription, modifier, variant, orientation, windowInsets) { … }, the items called in its content lambda. windowInsets defaults to WindowInsets.navigationBars for a docked toolbar and to none for a floating one. Compose has no toolbar role: the toolbar is a traversal group (isTraversalGroup) labelled by contentDescription, and the README says so. The roving Tab stop is TanreiRovingFocus, the primitive the list, menu and tabs use, horizontal or vertical by orientation; the toolbar registers each item it holds. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. Modifier.toolbarOverflow(TanreiToolbarOverflow.Never) on an item keeps it; the overflow menu is a TanreiMenu. TanreiToolbar(semanticLabel:, variant:, orientation:, children: [...]) (orientation is Flutter's Axis). A docked toolbar applies the bottom MediaQuery.padding itself, as SafeArea would, with its color under it. Flutter's widgets library has no toolbar role: the toolbar is a Semantics(container: true, explicitChildNodes: true, label:), and the README says so. The roving Tab stop is TanreiItemGroupMixin, the primitive the list, menu and tabs use, with its item axis set by orientation. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. An item wrapped in TanreiToolbarItem(overflow: TanreiToolbarOverflow.never, child:) is kept; the overflow menu is a TanreiMenu. <Toolbar variant orientation accessibilityLabel insets>{items}</Toolbar>, the items being Button elements, icon-only and labelled. The view has role="toolbar". A docked toolbar takes the bottom inset from its own insets prop, falling back to TanreiProvider's insets; the package takes no safe-area dependency. The roving Tab stop and the arrow keys work where a hardware keyboard is attached, as the tabs row's do. Lagging: the inline variant, the overflow menu, groups, separators, menu buttons and Buttons with menu as items are not built on this platform yet; it draws the docked and floating toolbar of icon-only and labelled Buttons, docked by default. toolbarOverflow="never" on an item keeps it; the overflow menu is a Menu.

Tokens

Token Aliases Resolved (light)
--tanrei-comp-toolbar-container-color-docked sys.color.surface #ffffff
--tanrei-comp-toolbar-container-color-floating sys.color.surface-bright #ffffff
--tanrei-comp-toolbar-container-color-inline — #000000 · 0%
--tanrei-comp-toolbar-container-elevation-docked sys.elevation.level0
--tanrei-comp-toolbar-container-elevation-floating sys.elevation.level3 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-toolbar-container-elevation-inline sys.elevation.level0
--tanrei-comp-toolbar-container-gap-docked — 16px
--tanrei-comp-toolbar-container-gap-floating sys.space.space0 0px
--tanrei-comp-toolbar-container-gap-inline sys.space.space50 4px
--tanrei-comp-toolbar-container-padding-inline-docked sys.space.space200 16px
--tanrei-comp-toolbar-container-padding-inline-floating sys.space.space150 12px
--tanrei-comp-toolbar-container-padding-inline-inline sys.space.space0 0px
--tanrei-comp-toolbar-container-shape-docked sys.shape.corner.none 0px
--tanrei-comp-toolbar-container-shape-floating sys.shape.corner.full 9999px
--tanrei-comp-toolbar-container-shape-inline sys.shape.corner.medium 8px
--tanrei-comp-toolbar-container-size-docked — 64px
--tanrei-comp-toolbar-container-size-floating — 64px
--tanrei-comp-toolbar-container-size-inline — 40px
--tanrei-comp-toolbar-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-toolbar-divider-length sys.space.space300 24px
--tanrei-comp-toolbar-divider-thickness sys.border.hairline 1px
--tanrei-comp-toolbar-group-gap sys.space.space100 8px
--tanrei-comp-toolbar-outline-color-docked sys.color.outline-variant #000000 · 8%
--tanrei-comp-toolbar-outline-color-floating — #000000 · 0%
--tanrei-comp-toolbar-outline-color-inline — #000000 · 0%
--tanrei-comp-toolbar-outline-width sys.border.hairline 1px
--tanrei-comp-toolbar-separation sys.behavior.separation 0

Fixtures

Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.

Generated from specs/toolbar.spec.json, tokens/comp/toolbar.tokens.json, design/tokens/

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 at the top. To move between the top-level destinations of an app, use the 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.

Floating: small, medium and large items, and narrow and wide ones

format_boldformat_italicformat_underlinedformat_boldformat_italicformat_underlinedformat_boldformat_italicformat_underlinedundoredoeditink_erasercheck

Items

  • Use icon-only Buttons 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 to a Button in the toolbar. For a main action with variations, such as Save and Save as, use a 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.

  • 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

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
The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px

Keyboard

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 / Space Activates the focused item, as that Button does; on the overflow button, opens the menu of the hidden items.

Notes

  • 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.

Generated from specs/toolbar.spec.json, tokens/sys/measure.tokens.json

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.