Components / structure
Toolbar
Forty-one of the toolbar's forty-six upstream refs test a menu button, a spinbutton and a radio group inside the toolbar. They are another pattern's contract, recorded as not applicable rather than ported twice.
Summary
A group of controls sharing one tab stop, with arrow keys moving inside it.
| APG pattern | Toolbar |
| Built on | native elements |
| Status | stable |
| Since | 0.1.0 |
<Toolbar label="Formatting">…</Toolbar>
Anatomy
A <div data-se="toolbar" role="toolbar"> holding controls and optional separators.
| Part | Element | Required |
|---|---|---|
group | the caller's | the caller's |
separator | <div> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
label | string | required | Required — a toolbar with no name is an unlabelled region. |
orientation | horizontal · vertical | horizontal | Which way the controls run, and therefore which arrow keys move between them. Announced as aria-orientation, so a screen reader tells the reader which keys to try. |
States
_None. Rest is the only condition it has._
Behavior
That single tab stop is the entire point. A toolbar of twelve buttons should cost a keyboard user one Tab, not twelve. A row of buttons in a plain div looks identical and is not a toolbar.
Roving focus is computed, not registered. The toolbar finds its items by looking for whatever is focusable inside it, so a composite child — a radio group, a menu button — is one item rather than invisible.
Arrows clamp at the ends rather than wrapping. The APG marks wrapping optional and this one clamps.
Accessibility
Every control still needs its own accessible name. Icon-only buttons carry label; the toolbar's own label names the group and is required, because an unnamed toolbar is an unnamed region.
A composite child is one stop. This is why Radix was removed here: it moves focus only between primitives it registered, so a nested RadioGroup was invisible to it — two tab stops instead of one, and arrows that stepped over the radios. Toolbar is native now, and must not go back on the Radix list.
Keyboard
| Key | Result |
|---|---|
Arrow Left / Arrow Right | Move between controls, clamping at the ends |
Arrow Up / Arrow Down | The same, for a vertical toolbar |
Home / End | Jump to the first or last control |
Tab | Leaves the toolbar — the whole set is one stop |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-label | set at render |
aria-orientation | set at render |
role | always toolbar, always separator |
Focus
One tab stop for twelve buttons, and that is the entire point. Roving focus is computed rather than registered: the toolbar finds its items by looking for whatever is focusable inside it, so a composite child — a radio group, a menu button — is one item rather than invisible. Radix could not do that, which is why it was removed here.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.
Tested
test/apg/batch-c.test.jsxtest/browser/upstream-toolbar-tree.spec.mjs
Known gaps
- 1 upstream ref — their toolbar controls a textarea. A toolbar controlling one named region is their example, not the pattern.
- 1 upstream ref — APG's own page or markup, not the pattern
- 36 upstream refs — tests the toolbar's contents; each is another pattern with its own suite
- 1 upstream ref — wrapping from the first control to the last. APG marks wrapping optional; ours clamps.
Appearance
| Token | Used for |
|---|---|
--se-border | background, border |
--se-border-width | block-size, border, inline-size |
--se-radius-md | border-radius |
--se-space-1 | gap, margin-block, margin-inline |
--se-space-2 | gap, padding |
--se-surface | background |
--se-target-min | min-block-size |
Motion
None of its own.
Content
The toolbar's label names the set — Formatting, Canvas tools.
Usage
When not to use it. For two or three unrelated actions. The cost of a toolbar is that a reader has to learn the arrow keys apply here; below about five controls that is a worse trade than five tab stops.
For joined actions that are each their own tab stop, use ButtonGroup.
Notes
ToolbarButton is a passthrough. The toolbar finds its focusable children itself, so wrapping a control is no longer what makes it reachable — it only says what the element is. Existing call sites keep working.
Forty-one of the toolbar's forty-six upstream refs test a menu button, a spinbutton and a radio group inside the toolbar. They are another pattern's contract, recorded as not applicable rather than ported twice.