Toolbar

A row of related controls — formatting buttons, view switches, a filter and a sort — that behaves as one tab stop with arrow keys between its controls, and folds what does not fit into an overflow Menu.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Toolbar } from '@design-schema/react';

Examples

import { Toolbar, ToolbarGroup, Button } from '@design-schema/react';

const twoGroups = (
  <>
    <ToolbarGroup label="Text style">
      <Button variant="ghost" label="Bold" overflowLabel="Bold" />
      <Button variant="ghost" label="Italic" overflowLabel="Italic" />
      <Button variant="ghost" label="Underline" overflowLabel="Underline" />
    </ToolbarGroup>
    <ToolbarGroup label="Insert">
      <Button variant="ghost" label="Link" overflowLabel="Link" />
      <Button variant="ghost" label="Image" overflowLabel="Image" />
      <Button variant="ghost" label="Table" overflowLabel="Table" />
    </ToolbarGroup>
  </>
);

<Toolbar label="Formatting">{twoGroups}</Toolbar>

Props

6 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

What the toolbar controls ("Formatting", "Table actions"). Not visible; read by assistive technology.

Accessibility: aria-label / accessibilityLabel on the toolbar.

web, lit, rn, swiftui
children requiredcontent—

Controls in order: Buttons (usually `ghost` or `secondary`, `iconOnly` for glyph tools), SegmentedControl, Select, Switch. Group related controls with `ToolbarGroup`; a Divider is drawn between two adjacent groups only (a bare control next to a group gets `itemGap`, no Divider), and only between top-level groups — a group nested inside a group takes no separator and no `size` pass. Consumers never place Dividers themselves. The Default and Keyboard stories are the same two labelled ToolbarGroups — "Text style" holding Bold, Italic and Underline, "Insert" holding Link, Image and Table, all ghost text Buttons — so a separator shows and the keyboard gate has its three focusable children; there is nothing to open, so the Keyboard story is the Default with `overflow: wrap` pinned, since the default `menu` can collapse controls out of the DOM at a narrow gate viewport.

web, lit, rn, swiftui
orientationhorizontal | verticalhorizontal

Vertical toolbars sit beside a canvas; arrow keys swap axes.

web, lit, rn, swiftui
overflowwrap | menu | scrollmenu

What happens when controls do not fit: wrap onto more rows, collapse trailing controls into a "More" Menu (each collapsible control must provide `overflowLabel`), or scroll horizontally with the edges faded. Collapsing takes whole entries from the end — a group goes into the Menu as a group, never half of one — and the width budget reserves `size.target.min` for the More trigger before it is rendered. `menu` is for horizontal toolbars; a vertical one treats it as `scroll`, since a menu overflow assumes a fixed cross axis. An entry collapses only if every control in it is a Button: walking from the end, an entry holding any other control is skipped and stays visible, even when a Button entry before it collapses. When the walk runs out of collapsible entries and the row still does not fit, collapsing stops and the row clips at the toolbar's edge — it never falls back to `scroll` and never collapses a non-Button. Each Menu item's id is the collapsed entry's place in the toolbar, `entry-<i>` for a top-level entry and `entry-<i>-<j>` inside a group, which is also the identity the missing-`overflowLabel` warning is keyed by, so reordering entries can warn twice about the same control. Web removes collapsed controls from the render; Lit, whose controls are the consumer's light DOM, sets `hidden` on them instead; both leave them out of the roving list. `wrap` on a vertical toolbar wraps onto more columns, which needs a bounded height from the parent to do anything and is otherwise the same as not wrapping.

web, lit, rn, swiftui
sizesm | mdmd

Default for the child controls that have a `size` prop — Button, SegmentedControl, Select and Search, recognised by component identity (web and React Native: the element type is the package component; Lit: the tag name), never by probing for a prop — and do not set their own. Applied to direct children and to the children of each ToolbarGroup, exactly one level deep: a control inside any other wrapper, and a group nested inside a group, are not reached into. A child's own `size` wins. Which sizes each control accepts is a fixed table, not something the toolbar probes for: Button, SegmentedControl and Select take `sm` and `md`, Search takes `md` only, so a `sm` toolbar leaves every Search at its own default (a later toolbar size means editing that table). On Lit a child counts as sized when it has a `size` attribute at the moment the toolbar discovers it; because every sized Lit control reflects a default `size`, the toolbar marks the children it sized with `data-ds-toolbar-size` on first sight and treats an unmarked child that already has the attribute as pre-sized. The overflow Menu's trigger takes no size (Menu has no `size` prop).

web, lit, rn, swiftui
densitycompact | comfortablecomfortable

Gap between controls: tight or normal rhythm.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Required props that carry accessibility meaning: label. Each is described in the props table above.

Implicit ARIA role: toolbar.

Anatomy

containergroupseparatoroverflowButtonoverflowMenu

What generation guarantees

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Roving tabindex — The group is a single tab stop; the arrow keys move within it.

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).

Toolbar keyboard support

6 rows
KeyAction
TabMoves focus into the toolbar (to the last-focused control, initially the first; when that control is gone — collapsed into the Menu or unmounted — the first control that is not disabled) and, from inside, out of it — the toolbar is one tab stop.
ArrowRightNext control (ArrowDown when vertical). Skips disabled controls; does not wrap.
ArrowLeftPrevious control (ArrowUp when vertical).
HomeFirst control.
EndLast control.
Enter or SpaceActivates the focused control (its own behavior).

Contrast pairs

3 rows
ForegroundBackgroundLevel
color.foregroundcolor.background.subtleAA
color.foreground.mutedcolor.background.subtleAA
color.action.ghost.foregroundcolor.background.subtleAA

This component does not animate, so it has no reduced-motion behaviour to describe.