v1.0.0

<bmx-split-button>

A default action joined to a menu of alternatives - "Save" with "Save as…" and "Save a copy" behind the chevron. The pattern earns its keep wherever there is one obvious action and three rarer ones: it keeps the obvious one a single click away instead of demoting it into a dropdown with its siblings.

11 properties · 3 events · 2 methods · 6 parts

Example

Save Export
Show markup
<div class="row">
  <bmx-split-button id="docs-split-save" label="Save">Save</bmx-split-button>
  <bmx-split-button id="docs-split-export" variant="outline" tone="neutral" label="Export" placement="bottom-end">Export</bmx-split-button>
</div>
<script>
  // `items` is an array, so it is set as a property. An HTML attribute can only
  // ever be a string; an array pushed through one arrives as "[object Object]".
  //
  // Wait for the element to be defined first. This script runs while the page
  // parses, which is before the deferred runtime has registered anything, and a
  // property set on an element that has not upgraded yet may not survive the
  // upgrade. `whenDefined` is the reliable order, and it is what you want in
  // your own application too.
  customElements.whenDefined('bmx-split-button').then(() => {
    document.getElementById('docs-split-save').items = [
    { id: 'save-as', label: 'Save as…', shortcut: '⇧⌘S' },
    { id: 'save-copy', label: 'Save a copy', description: 'Keeps the original untouched' },
    { separator: true },
    { id: 'discard', label: 'Discard changes', danger: true },
  ];

    document.getElementById('docs-split-export').items = [
      { id: 'xlsx', label: 'Excel workbook' },
      { id: 'csv', label: 'CSV', shortcut: '⌘E' },
      { id: 'pdf', label: 'PDF', disabled: true },
    ];
  });
</script>

WHAT IS LEFT HERE

The two halves, and the debt-free part of the relationship between them. The menu is bmx-menu, which owns the surface, the placement, the roving tabindex, the typeahead and the focus. This component owns its trigger, and translates between the menu's vocabulary and its own.

That translation is not ceremony. Every bmx- event is composed, so a bmxSelect emitted by the menu inside this shadow root would reach a consumer of the split button alongside the one this component emits - and the consumer would see every choice twice. Each inner event is therefore stopped at the boundary and re-published deliberately, which is the rule in §8: an event that escapes a shadow root is part of that component's API whether it was meant to be or not.

HOW THE DELEGATION WAS MADE SAFE

The menu used to be built into this file. It was taken out under fifty-three characterisation tests written against the behaviour as it stood - including five that do nothing but count events - and not one of them was edited to accommodate the move. The single exception is recorded in the test file itself: the menu now answers a typed letter, because bmx-menu has the typeahead the WAI-ARIA menu pattern asks for and this component never did.

SUBMENUS

An entry with its own items opens a submenu, to any depth, exactly as it does in bmx-menu - "Export" with three formats under it is the case that earns it. That arrived with the delegation rather than being designed here, which made it worth a deliberate decision: either test and document it or refuse it, because an undocumented accident is the one option that is wrong. It is documented, and the choice reports a path so a nested item is identifiable. Keep them shallow. A split button is a short list of alternatives to one obvious action, and a menu deep enough to need navigating is a sign the obvious action is not obvious.

WHY THE MENU IS ANCHORED TO THE WHOLE COMPONENT

Aligning to the chevron alone would hang a wide menu off a narrow button; aligning to both halves is what makes bottom-start line up with the primary action's leading edge, which is the edge the eye is already on. Focus, by contrast, belongs on the chevron the user actually pressed - which is why the menu is told those two things separately.

ACCESSIBILITY

The trigger carries aria-haspopup="menu" and aria-expanded and points at the surface the menu renders; everything inside the menu - the roles, the roving tabindex, focus moving in on open and back to the trigger on close - is bmx-menu's, and is the same in every component that uses it.

Properties

PropertyAttributeTypeDefaultDescription
action property only (event: unknown) => unknown | Promise<unknown> An async handler for the primary action, driving its busy state.
disabled disabled boolean false Disable both halves.
items property only BmxMenuEntry[] [] The menu's entries. A property, because it is an array. Accepts the JSON spelling of the list as well, because an attribute is the only channel some templates have. See src/core/markup.ts.
label label string The primary action's accessible name, when it has no visible label.
loading loading boolean false Show the primary action as busy.
menuLabel menu-label string 'More actions' The trigger's accessible name.
placement placement 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' 'bottom-start' Which side the menu opens on when there is room.
shape shape BmxShape 'rounded' Corner treatment of the outer edges.
size size BmxSize 'md' Size step.
tone tone BmxTone 'primary' Semantic colour role.
variant variant BmxButtonVariant 'solid' Visual treatment, applied to both halves.

Events

EventDetailDescription
bmxActivate void Fired when the primary action is activated.
bmxOpenChange boolean Fired when the menu opens or closes.
bmxSelect BmxSplitButtonSelectDetail Fired when a menu item is chosen.

Methods

MethodSignatureDescription
closeMenuNow closeMenuNow() => Promise<void> Close the menu. Focus returns to the trigger.
openMenu openMenu() => Promise<void> Open the menu and move focus into it.

Slots

SlotDescription
(default) The primary action's label.
start An icon for the primary action.

CSS shadow parts

PartDescription
action The primary action button.
base The container.
item A menu item.
menu The menu surface.
separator A divider between items.
trigger The menu trigger button.

CSS custom properties

PropertyDescription
--bmx-button-radius Corner radius of the outer edges. The shared edge between the two halves is always square.
--bmx-effect-duration Passed through to both halves' click effects.