Surfaces
Toolbar
The bar of actions at the edge of a working surface.
Usage
When to reach for it
import { Toolbar } from '@misoto22/design'Notes
The bar of actions at the edge of a working surface.
Two page templates built the same thing independently — a sticky strip on --paper with a rule along the edge it sticks to — which is the signal that it belongs here rather than in each of them.
The ground is opaque --paper and not a blur. Content scrolls UNDER this bar, so anything translucent puts the last row of a table behind the submit button and makes both unreadable; FloatingIconButton blurs because it floats over a gap, which is a different problem.
It is not role="toolbar". That role's contract is a single tab stop with arrow keys moving between the controls inside it, and this implements no such thing — declaring the role without the behaviour tells a screen-reader user to press arrow keys that do nothing. It is a named group, so the controls keep their own places in the tab order and the bar is still announced.
Anatomy
| Element | Description |
|---|---|
| Barrequired | A <div role="group"> named by label, wrapping its children on a flex row at --z-sticky. It is not role="toolbar": that role promises a single tab stop with arrow keys between the controls, and this implements no such thing. |
| Groundrequired | Opaque --paper, and deliberately not a blur. Content scrolls UNDER this bar, so anything translucent puts the last row of a table behind the submit button and makes both unreadable. |
| Edgerequired | A --rule-2 hairline on the side the bar sticks to: border-t for bottom, border-b for top. position="static" keeps the rule and drops the stickiness. |
| Actionsrequired | children, on a flex-wrap row with a --gap of 3. align places them along the inline axis and defaults to end, which is where a form’s primary action goes. |
Best practices
Do
- Write label as what the bar IS — "Form actions", "List filters". A group with no name is announced as "group", and a page with two of them announces the same nothing twice.
- Give the scrolling ancestor a height for position="bottom". A sticky element sticks within its scroll container, so a bar inside a container that is as tall as its content has nothing to stick to and simply sits at the end.
- Keep it to the actions. A bar that has grown a title, a status and a breadcrumb is a page header, and a page header that follows the reader down the screen is a page with less of itself visible.
Don’t
- Do not add role="toolbar" through props. The role tells a screen-reader user that arrow keys move between the controls; nothing here implements roving tabindex, so those keys would do nothing and the promise would be false.
- Do not make the ground translucent to "let the content show through". The content it would show through is the row the reader is trying to read, and the button they are trying to press.
Examples
form actions
The bar of actions at the foot of a working surface. The ground is opaque --paper rather than a blur, because content scrolls under it — anything translucent puts the last row of the form behind the submit button. It is a named group and not role="toolbar": that role promises arrow keys between the controls, and nothing here implements them.
Scroll this panel — the bar below stays where it is.
A sticky element sticks within its scroll container, so the container needs a height of its own for the bar to have anything to stick to.
Keep the bar to the actions. One that has grown a title and a breadcrumb is a page header following the reader down the screen.
a filter bar on top
The same component at the other edge. position top sticks the bar to the head of its scroll container and moves the hairline to the bottom, so a reader scrolling a long list keeps the controls that shaped it. align between is what the shape needs here: the filters read from the start edge and the action sits at the end, which is one bar rather than a filter row and a button row. A sticky element sticks inside its scroll container, so the container is the thing that needs a height — not the bar.
- a1b2c3d
- 9f8e7d6
- 4c5b6a7
- 77aa2b1
- e3d4c5b
- 0099aab
- 12ff34e
every control is a tab stop
A bulk-action bar, and the reason the role is group rather than toolbar. The ARIA toolbar contract is a single tab stop with the arrow keys moving between the controls inside it; this implements no roving tabindex, so declaring that role would tell a screen-reader user to press keys that do nothing. As a named group every control keeps its own place in the tab order — Tab reaches Archive, then Delete, then Clear — and the bar is still announced by name, which is why label is required rather than optional.
Types
export type ToolbarPosition = 'bottom' | 'top' | 'static'
export type ToolbarAlign = 'start' | 'center' | 'end' | 'between'Keyboard
| Key | Does |
|---|---|
| Tab | Reaches each control in turn — the bar itself is not a stop. |
Accessibility
- label is required and becomes the group’s accessible name, so a page with a filter bar and an action bar announces two distinct things.
- Every control keeps its own place in the tab order, because the bar deliberately does not claim role="toolbar" and its single-tab-stop contract.
- The ground is opaque, so a control on the bar always meets its contrast ratio against --paper rather than against whatever happens to be scrolling behind it.