v1.0.0

<bmx-toolbar>

A bar of controls that does not break when the window does. When there is no longer room for everything, the items that do not fit move into a menu behind one button - and they keep working from there, because choosing one activates the element you wrote rather than a copy of it.

5 properties · 1 events · 2 methods · 3 parts

Example

Drag the handle to narrow the bar, and watch what does not fit move into the menu:

Bold Italic Underline
Bullet list Numbered list
Insert link Insert image Publish
Publish carries data-bmx-fixed, so it never collapses however narrow the bar gets.
Show markup
<div class="row">
  <p style="margin: 0">Drag the handle to narrow the bar, and watch what does not fit move into the menu:</p>
</div>

<div class="row">
  <div id="ex-toolbar-frame" style="inline-size: 100%; resize: horizontal; overflow: auto; min-inline-size: 12rem; padding-block-end: 8px">
    <bmx-toolbar id="ex-toolbar" label="Formatting">
      <bmx-button variant="ghost" tone="neutral">Bold</bmx-button>
      <bmx-button variant="ghost" tone="neutral">Italic</bmx-button>
      <bmx-button variant="ghost" tone="neutral">Underline</bmx-button>
      <hr />
      <bmx-button variant="ghost" tone="neutral">Bullet list</bmx-button>
      <bmx-button variant="ghost" tone="neutral">Numbered list</bmx-button>
      <hr />
      <bmx-button variant="ghost" tone="neutral">Insert link</bmx-button>
      <bmx-button variant="ghost" tone="neutral">Insert image</bmx-button>
      <bmx-button data-bmx-fixed>Publish</bmx-button>
    </bmx-toolbar>
  </div>
</div>

<div class="row">
  <span class="note" id="ex-toolbar-out">
    <strong>Publish</strong> carries <code>data-bmx-fixed</code>, so it never collapses however narrow the bar gets.
  </span>
</div>

<script type="module">
  await customElements.whenDefined('bmx-toolbar');

  const out = document.getElementById('ex-toolbar-out');

  /*
   * Every button has one handler, and it does not care whether the press came
   * from the bar or from the overflow menu: choosing an item in the menu calls
   * the original element's own `activate()`, so this listener hears both.
   */
  for (const button of document.querySelectorAll('#ex-toolbar bmx-button')) {
    button.addEventListener('bmxActivate', () => {
      out.textContent = `pressed: ${button.textContent.trim()}`;
    });
  }
</script>
<bmx-toolbar label="Formatting">
  <bmx-button variant="ghost">Bold</bmx-button>
  <bmx-button variant="ghost">Italic</bmx-button>
  <hr />
  <bmx-button variant="ghost">Insert link</bmx-button>
  <bmx-input data-bmx-fixed placeholder="Search"></bmx-input>
</bmx-toolbar>

WHY NOT bmx-button-group

They look alike and answer different questions. A button group is a set of related buttons drawn as one control - a segmented view switcher, a run of toggles - and it is selection that makes it interesting. A toolbar is a bar: mixed controls, dividers between them, and one thing a group has no opinion about, which is what happens when the bar is narrower than its contents. Put groups inside a toolbar; they collapse as single items.

WHAT COLLAPSES, AND IN WHAT ORDER

From the end, always. Never by size and never by a guess at importance: a bar that reshuffled itself as the window moved would make "the third button" worthless as a thing to remember. Mark anything that must survive with data-bmx-fixed - a search field, a primary action - and its room is taken off the top. The arithmetic is src/core/toolbar.ts, tested without a browser.

HOW AN OVERFLOWED ITEM STILL WORKS

The menu is a proxy, not a copy. Choosing an entry calls the original element's own activate() where it has one - every bmx-button does - and otherwise clicks it. So the handler you wrote runs, whatever it is, and there is no second code path to keep in step with the first. The menu's label for an item comes from data-bmx-label, then the element's label or aria-label, then its text.

THE KEYBOARD

One Tab stop, arrows within, as WAI-ARIA's toolbar pattern asks. The overflow trigger is the last stop on the bar. A bmx-button is told its tabindex through rovingTabindex, because the focusable element inside it is in a shadow root this component cannot reach; a plain focusable element gets its tabIndex set directly. A control with its own inner keyboard - a text field

  • keeps its own tab stop rather than being folded into the roving set, which is what the pattern asks for and what a user typing into it expects.

Properties

PropertyAttributeTypeDefaultDescription
collapse collapse 'auto' | 'never' 'auto' Whether items that do not fit move into a menu. never leaves the bar to overflow however the page's CSS says it should - which is the right answer when the toolbar is inside something that already scrolls.
label label string 'Toolbar' The toolbar's accessible name.
menuLabel menu-label string 'More' The overflow button's accessible name.
orientation orientation BmxOrientation 'horizontal' Which way the bar runs.
wrapFocus wrap-focus boolean true Whether the arrows wrap from one end of the bar to the other.

Events

EventDetailDescription
bmxSelect BmxToolbarSelectDetail Fired when an item is chosen from the overflow menu.

Methods

MethodSignatureDescription
refresh refresh() => Promise<void> Re-measure now. Rarely needed: a resize and a change of children are watched.
setFocus setFocus() => Promise<void> Focus the toolbar, landing on whichever item holds its tab stop.

Slots

SlotDescription
(default) The toolbar's controls, and <hr> elements as dividers.

CSS shadow parts

PartDescription
base The bar.
menu The overflow menu's surface.
trigger The overflow button.

CSS custom properties

PropertyDescription
--bmx-toolbar-background The bar's background.
--bmx-toolbar-border-color The bar's border, and the dividers on it.
--bmx-toolbar-gap Space between one item and the next.
--bmx-toolbar-padding Space inside the bar.
--bmx-toolbar-radius Corner radius of the bar.
--bmx-toolbar-separator-inset How far a divider stops short of the bar's edges.