Skip to content

HyMenuButton ​

Usage examples on this page are written for Lit / plain HTML (<hy-menu-button>). The same component ships as HyMenuButton in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A trigger + menu pair implementing the ARIA menu-button pattern. The trigger is any element slotted into trigger; the menu surface is the child hy-menu (rendered in the default slot) or an actions shorthand.

Trigger shapes ​

  • Slotted trigger — put any element (hy-button, hy-icon-button, chip, avatar, …) into slot="trigger". You own the label, icons, and styling.
  • Default trigger — when label is set and slot="trigger" is empty, the component renders an internal hy-button caret truncate with the label text and a trailing chevron. The truncate flag means long labels clip with an ellipsis instead of overflowing the row, so the default trigger is safe to use as a "value picker" reflecting an unbounded current selection. Slotted triggers don't get this for free — set truncate on your own hy-button if you need it.

Composition contract ​

Slotted hy-menu-item children always win over the actions array — same precedence as hy-menu-item's own label prop losing to the default slot. When both are set in dev mode, a single console warning fires per element.

ARIA wiring ​

The trigger receives aria-haspopup="menu" and aria-expanded; the default trigger also an aria-controls pointing at the internally managed menu id (a slotted trigger gets none — its id reference could not reach the menu in this element's shadow root). Clicks on the trigger toggle the menu; Escape from the menu returns focus to the trigger (delegated to hy-popover). A host aria-label forwards to the default trigger, naming the control when the visible label is a current value (e.g. a picker showing "solid") rather than what the control does.

Split button or menu button? ​

When one action is the usual choice and the menu holds its variations (Save, then Save as… and Save a copy), use hy-split-button: the main action stays one click. When no action is the default, use a menu button.

When to use

  • A set of commands that would crowd the page if shown as buttons: an item's More actions.
  • Secondary commands next to a visible main action.

Do

  • Name the trigger by what the menu holds: "Export", "More actions".
  • Put destructive commands last, set apart from the others.
  • Group a long menu with hy-menu-group.

When not to use

  • Only one command: a plain hy-button.
  • A command people use all the time: its own visible hy-button, outside the menu.
  • Choosing a value a form reads: hy-select or hy-combobox. A menu announces commands, not options.
  • A set of destinations: hy-sidebar, hy-tab-nav or hy-link.
  • A trigger that cannot sit in the trigger slot: hy-menu with for set to the trigger's id.

Accessibility: your part ​

  • When the trigger is an hy-icon-button, give it a label and an hy-tooltip with the same name.

Examples ​

Slotted trigger with a divider and a danger action ​

html
<hy-menu-button>
  <hy-button slot="trigger">
    Actions
    <hy-icon slot="suffix" name="chevron-down"></hy-icon>
  </hy-button>
  <hy-menu-item value="edit" label="Edit"></hy-menu-item>
  <hy-menu-item value="duplicate" label="Duplicate"></hy-menu-item>
  <hy-menu-item value="archive" label="Archive"></hy-menu-item>
  <hy-divider></hy-divider>
  <hy-menu-item value="delete" label="Delete" variant="danger"></hy-menu-item>
</hy-menu-button>

Default trigger rendered from the label prop ​

html
<hy-menu-button label="Sort by" aria-label="Sort order">
  <hy-menu-item value="name" label="Name"></hy-menu-item>
  <hy-menu-item value="date" label="Date modified"></hy-menu-item>
  <hy-menu-item value="size" label="Size"></hy-menu-item>
</hy-menu-button>
<script>
  document.querySelector('hy-menu-button').addEventListener('select', (e) => {
    console.log(e.detail.value);
  });
</script>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
openopenbooleanfalseControlled open state.
placementplacement'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end''bottom-start'Placement forwarded to the child menu.
sizesize'small' | 'medium''medium'Density variant forwarded to the child hy-menu (and through it to every menu item). Also sizes the default hy-button trigger (when label renders one). Does not re-size a slotted trigger — consumers size their own trigger element.
labellabelstring''Label used by the default hy-button caret trigger. Ignored when slot="trigger" has content.
widthwidthstring''Fixed menu-panel width (any CSS length, e.g. '220px'). Forwarded to the child hy-menu.
minWidthmin-widthstring''Minimum menu-panel width. Forwarded to the child hy-menu.
fullWidthfull-widthbooleanfalseStretch the trigger to fill the host's inline size. Off by design; opt in for a full-width "select-style" dropdown trigger. The slotted (or default truncate) trigger grows to match. Distinct from width / minWidth, which size the dropped menu panel.
actions—MenuAction[][]Shorthand for rendering items from data. When set, items are rendered only if the default slot is empty. Slotted children always win.

Events ​

EventDetailDescription
select—Relayed from the child menu. Detail: MenuSelectDetail.
change—Relayed from the child menu when a radio group changes. Detail: MenuChangeDetail — { selectedValue, name, reason }.
show—Relayed from the child menu (does NOT bubble).
hide—Relayed from the child menu (does NOT bubble).

Slots ​

SlotDescription
triggerThe trigger element (button, icon-button, chip, avatar, …). When empty and label is set, an internal hy-button caret truncate is rendered (long labels clip with an ellipsis).
defaulthy-menu-item / hy-menu-item-checkbox / hy-menu-item-radio / hy-menu-group / hy-divider children.

CSS Parts ​

PartDescription
trigger-wrapperThe wrapper element around the trigger slot.

Methods ​

setTabIndex() ​

Projects a tabindex onto the trigger — called by a parent roving-tabindex controller (hy-toolbar) so the menu button joins its single tab stop.

focus() ​

Focuses the trigger.

Built with Lit. Documented with VitePress.