<ui-toolbar>
A toolbar groups related controls (buttons, links, selects) into a single
keyboard-navigable strip using the
WAI-ARIA APG Toolbar
pattern and roving tabindex. Use <ui-toolbar> when
controls belong together conceptually and you want arrow-key navigation within
the group; use an ordinary <menu> or a <nav>
when the controls are standalone links that don't need arrow-key movement.
Source: ui-toolbar.mjs.
role="toolbar" is set on connect (and preserved if the host
already carries a different role such as menubar).aria-label (or aria-labelledby) is
required so the toolbar has an accessible name (WCAG
SC 4.1.2 — Name, Role, Value). Every demo on this page provides one.tabindex="0";
the rest carry tabindex="-1". Tab moves focus into the
toolbar at the active item; arrow keys move within (WCAG SC 2.4.3 —
Focus Order).aria-orientation: Left/Right for
"horizontal" (default), Up/Down for "vertical".
Arrows on the other axis retain their usual behaviour, so e.g. Up/Down still scrolls
the page when focus is in a horizontal toolbar (WCAG SC 2.1.1 —
Keyboard)..items, which the next
bullet defines.disabled — skipped. Excluded from
.items, so arrow-key navigation skips it. The control remains visible but cannot receive keyboard focus.aria-disabled="true" — navigable. Kept in
.items, so it stays arrow-reachable, focusable and announced
as unavailable — and can explain why it is unavailable through its tooltip. It is
not activatable: a click on one is stopped by the component, as the APG requires for focusable disabled controls.disabled: this keeps keyboard navigation shorter. See
§3.hidden, or sitting inside an element that carries it, is left out of
.items, so the arrows, Home and End pass over it. This is
not a choice the page makes: the browser refuses focus to an element it does not render, so
an arrow press onto one would move the roving tabindex and leave focus where it was — the
user presses twice to move one place (WCAG SC 2.4.3 — Focus Order). Show the item again and
it is back in the arrow order with no further call.
It is the attribute that is read, not the used style:
ui.css backs hidden with
display: none !important, which makes the attribute the one form of hiding a
page cannot undo by accident. An item hidden with a class or a media query is still an
arrow stop; use the attribute for one that should not be.tabindex over every item matching selector, including the
disabled and hidden ones, so a control disabled or hidden while it held the stop
is given tabindex="-1" rather than leaving the toolbar with two. Call
refresh_tabstop() after that change and the stop moves to the nearest
navigable item after it — one place along, not back to the start of the toolbar. A toolbar
whose items are all unnavigable still keeps one stop parked, so Tab
reaches it as soon as one of them is shown or enabled.aria-disabled
— a natively-disabled element cannot be focused at all, by any
means, so use the appropriate attribute for the desired behaviour. The same distinction applies to
.btn,
tooltip() and
role="switch", which all use
aria-disabled for controls that must remain focusable.| Element | Attributes | Properties | Methods | Events |
|---|---|---|---|---|
<ui-toolbar> |
aria-orientation, aria-label, selector |
.items |
refresh_tabstop() |
ui-toolbar-focus |
selector defaults to :scope > button, so without it the toolbar
manages only its direct <button> children. Nested buttons and links are
left alone.
The component sets focus behaviour, not layout.
ui.css has no rule for <ui-toolbar>,
so it displays inline and its items flow like ordinary text. The demos on this page get their
row and column layout from this page's own stylesheet:
ui-toolbar { display: flex; gap: var(--size-1); }, plus
flex-direction: column for aria-orientation="vertical". Copy that
rule when you reuse a demo.
Horizontal toolbar with three buttons. Left/Right arrow keys move focus.
Vertical toolbar with aria-orientation="vertical". Up/Down arrow keys navigate.
Both buttons below are unavailable and both are dimmed, so they look alike. They
behave differently on purpose: Skipped carries
disabled and arrow-key navigation skips it, while
Navigable carries aria-disabled="true" and the arrows
stop on it. Tab into the row and use the arrows to move across it — the log records focus and click events.
Navigable is appropriate when discovering the disabled functionality
matters, which is the criterion the
APG Toolbar pattern
names: keyboard and screen-reader users can focus the control to discover it and read its tooltip explanation. It has to be aria-disabled rather than disabled
because a natively-disabled element cannot be focused at all — so keeping a control focusable requires an alternative to disabled.
Navigable does not mean activatable: <ui-toolbar> stops a click on
such an item, so it cannot be activated, consistent with its announced state.
The selector attribute controls which elements are managed. Here with selector="a".
Add/remove buttons dynamically. Call refresh_tabstop() to re-initialise the roving tabindex.
Focus the toolbar and navigate with arrow keys, Home and End. Events are logged.
When another part of the page needs to reflect the toolbar’s focused item
— a preview pane, a second window, a detail panel — listen for
ui-toolbar-focus. It carries detail: {item, index}, bubbles,
and fires for every way an item can take focus: the arrows, Home/End,
a click, and Tab into the toolbar.
Prefer it over a plain focusin listener. It fires only
for managed items, so a focusable element outside the selector —
the search field below, a control nested inside an item, a nested
listbox()'s options — does not produce this event. With a general focus listener, each consumer would need to check the selector itself. Focus each control below and compare the two log lines.
ui-toolbar-focus reports focus changes. It does not fire for refresh_tabstop(), which resets the tab stop without a user gesture. This prevents a preview from resetting whenever the toolbar re-renders.
Mirror: nothing yet