Navigation
Toolbar
A strip of tools inside a panel: modes, filters, small actions on its content. Not a page header and not a menu.
Markup
<div class="inst-panel">
<div class="inst-panel-header"><span class="inst-panel-title">Inspector</span></div>
<div class="inst-toolbar">
<button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Move">
<svg class="inst-icon" aria-hidden="true"><use href="#i-move"/></svg></button>
<button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Rotate">
<svg class="inst-icon" aria-hidden="true"><use href="#i-rotate"/></svg></button>
<button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon" type="button" aria-label="Scale">
<svg class="inst-icon" aria-hidden="true"><use href="#i-scale"/></svg></button>
<span class="inst-toolbar-sep"></span>
<span class="inst-toolbar-spacer"></span>
<span class="inst-badge">terrain_chunk_04</span>
</div>
<div class="inst-panel-body">…</div>
</div>
Contract
| What | Required | Why |
|---|---|---|
A type="button" on every button |
yes | Otherwise inside a form it will submit it |
An aria-label on an icon button |
yes | A button with no text has no accessible name: "Move" rather than "Tool 1" |
An aria-hidden="true" on the icon |
yes | Otherwise a screen reader reads it twice or does not read it at all |
An aria-pressed on a mode button |
yes, if the tool stays switched on | The state has to be in the markup. Set by the application |
One inst-toolbar-spacer per strip |
yes | The spacer sets off the tail — a status, a counter, a close. There is no such thing as two points of break in one strip |
The toolbar role is not assigned automatically: it promises arrow navigation
and a roving tabindex, and that behaviour belongs to the application layer. A
strip of ordinary buttons with no role is accessible without the promises.
Accessibility
| The keyboard | Every button is traversed by Tab. Arrow navigation within the strip comes only along with a role="toolbar" and a roving tabindex, that is, with the application's JS |
| The names | Icon buttons have to carry an aria-label: "Move" rather than "Tool 1" |
| The tap target | inst-btn--sm is 26px. At density compact it is 22px, below the 24px threshold of WCAG 2.5.8 |
| The divider | An empty element with no text and no role — it is not in the accessibility tree, and that is right: it says nothing beyond the order |
| Contrast | The lines --border-subtle and --border are decoration (a 3:1 threshold) rather than text |
| A toolbar is not printed: it is the chrome of the application rather than data |
Anatomy
Two service elements that are easy to confuse:
| Element | What it does | When |
|---|---|---|
inst-toolbar-sep |
A vertical line the full height of the strip | Between meaningful groups of tools: the transforms apart, the display apart |
inst-toolbar-spacer |
margin-inline-start: auto — everything after it travels to the far edge |
Once per strip, before the tail: a status, a counter, a close button |
The divider is an empty <span>, that is, pure graphics with no text and no
role. It shows the grouping to the eye; for a screen reader the grouping is
carried by the order and by the labels of the buttons.
Composition
Its place in a panel
A toolbar stands between the header and the body of a panel and brings the
line below itself. The header then removes its own line
(:has(+ .inst-toolbar)) — otherwise there would be two rules in a row within
thirty pixels.
Markup
<div class="inst-panel">
<div class="inst-panel-header"><span class="inst-panel-title">Modules</span></div>
<div class="inst-toolbar">
<div class="inst-segmented" role="radiogroup" aria-label="View">
<button type="button" role="radio" aria-checked="true" tabindex="0">List</button>
<button type="button" role="radio" aria-checked="false" tabindex="-1">Grid</button>
</div>
<span class="inst-toolbar-sep"></span>
<span class="inst-search">
<input class="inst-input inst-input--sm" type="search" placeholder="Filter" aria-label="Filter the modules">
</span>
<span class="inst-toolbar-spacer"></span>
<span class="inst-badge">3 of 12</span>
</div>
<div class="inst-panel-body">The content of the region.</div>
</div>
A toolbar with no header is legitimate too: a panel that has no name but has modes.
API
| Name | What it does |
|---|---|
| class | |
inst-toolbar | The strip. A row with a gap of --space-3 and a line below |
inst-toolbar-sep | A vertical divider between groups |
inst-toolbar-spacer | Pushes the tail of the strip to the far edge |
| Name | Value | Where from |
|---|---|---|
| token | ||
--space-2 | 4px | one across every cell |
--space-3 | 6px | one across every cell |
--row-pad-y | var(--space-3) | base +14: density · scale |
--hairline | 1px | base +1: screen |
--border-subtle | light-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06)) | one across every cell |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | one across every cell |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument