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| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | What the toolbar controls ("Formatting", "Table actions"). Not visible; read by assistive technology. Accessibility: aria-label / accessibilityLabel on the toolbar. |
| children required | content | — | 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. |
| orientation | horizontal | vertical | horizontal | Vertical toolbars sit beside a canvas; arrow keys swap axes. |
| overflow | wrap | menu | scroll | menu | 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. |
| size | sm | md | md | 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). |
| density | compact | comfortable | comfortable | Gap between controls: tight or normal rhythm. |
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
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).
This component does not animate, so it has no reduced-motion behaviour to describe.