Toolbar
Toolbar — a grouped row of controls acting on the current view: a text editor's formatting bar, a table's filter row, the actions along the top of a card.
Examples
<div class="toolbar" role="toolbar" aria-label="Text formatting" data-ui="toolbar">
<div class="toolbar-group" role="group" aria-label="Style">
<button class="btn btn-quiet btn-icon" type="button" aria-pressed="true" aria-label="Bold">
<svg class="icon" aria-hidden="true"><use href="#icon-bold"></use></svg>
</button>
<button class="btn btn-quiet btn-icon" type="button" aria-pressed="false" aria-label="Italic">
<svg class="icon" aria-hidden="true"><use href="#icon-italic"></use></svg>
</button>
<button class="btn btn-quiet btn-icon" type="button" aria-pressed="false" aria-label="Underline">
<svg class="icon" aria-hidden="true"><use href="#icon-underline"></use></svg>
</button>
</div>
<hr class="divider divider-vertical" aria-orientation="vertical" />
<input class="input" type="search" aria-label="Find in document" placeholder="Find" />
<span class="toolbar-spacer"></span>
<button class="btn btn-quiet" type="button">Clear</button>
</div>role="toolbar" promises one tab stop and arrow keys, so it ships with data-ui="toolbar" or not at all. The trap is the text field: arrow keys inside it must move the caret, not the toolbar.
<div class="toolbar">
<button class="btn btn-quiet" type="button">Duplicate</button>
<button class="btn btn-quiet" type="button">Archive</button>
<hr class="divider divider-vertical" aria-orientation="vertical" />
<button class="btn btn-quiet" type="button">Delete</button>
</div>For three or four buttons this is usually the better choice.
role="toolbar" earns its keep when the bar is long enough that tabbing through it is a burden — below that it trades a familiar Tab for an arrow-key model the user has to discover.
Tokens 11
Level 2, declared on .toolbar itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --toolbar-group-gap | var(--space-1) |
| --toolbar-separator-color | var(--color-border) |
| --toolbar-separator-spacing | var(--space-1) |
| --toolbar-bg | var(--color-surface) |
| --toolbar-border-color | var(--color-border-subtle) |
| --toolbar-border-width | var(--border-width) |
| --toolbar-color | var(--color-on-surface) |
| --toolbar-gap | var(--space-2) |
| --toolbar-padding-block | var(--space-2) |
| --toolbar-padding-inline | var(--space-2) |
| --toolbar-radius | var(--radius-box) |
Variants and states
Variants
.toolbar-plain
Inside it
.toolbar-group.toolbar-scroll.input.select.toolbar-spacer.toolbar-vertical.divider.divider-vertical
State it reads
None.
Before you ship it
This one needs a script. Opt the markup in with data-ui="toolbar" and import import 'mostlycss/js/components/toolbar' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.
What you have to do 1 requirements
- Pick one of the two markups above and copy all of it. Then: Name the bar in markup 1.
aria-labelon therole="toolbar"— "Text formatting", "Row actions". A toolbar with no name announces as "toolbar" and nothing else, which is useless on a page with two of them. Name every icon-only button, on the button and never on the<svg>. A toolbar is where icon-only controls cluster, so this is where the omission costs most: a bar of nine unnamed buttons is nine identical announcements. A toggle button carriesaria-pressed, and nothing else. button.css already styles the pressed state from that attribute, so there is no class to add and no state to mirror.aria-pressed="false"must be present when the button is off — omitting it leaves a plain button rather than an unpressed toggle. Usearia-checkedonly inside arole="radiogroup", and.segmentedis the component for that.
src/css/components/toolbar.css · npx mostlycss add toolbar