Skip to content
HyperUX Experimental
Demo

Dropdown menu behavior with keyboard movement, menu-item focus management, and action dispatch.


Alpine.js Dropdown

huxDropdown provides menu-button behavior with open/close state, focus movement, and menu item selection handling for link and action items.

API

huxDropdown(config)

Returns an Alpine data object with:

Internal helper methods are private implementation details and are not part of the supported API contract.

Options

Use hyphen-case for action names. Non-hyphen-case names will be ignored.

Quick Start

<div
  x-data="huxDropdown({
    triggerId: 'user-menu',
    menuItems: [
      {
        label: 'Account',
        href: '/account',
        children: [{ label: 'Billing', href: '/billing' }]
      },
      { label: 'Copy Link', action: 'copy-link' }
    ]
  })"
  x-on:click.outside="closeMenu()"
  x-on:hux-dropdown:user-menu:copy-link.window="navigator.clipboard.writeText(location.href).catch(() => {})"
>
  <button
    x-ref="dropdownTrigger"
    type="button"
    x-bind:id="triggerButtonId"
    x-bind:aria-expanded="isOpen.toString()"
    x-bind:aria-controls="menuId"
    aria-haspopup="menu"
    x-on:click="toggleMenu()"
    x-on:keydown.down.prevent="openMenu(); focusFirstItem()"
    x-on:keydown.up.prevent="openMenu(); focusLastItem()"
  >
    Open menu
  </button>

  <div
    x-cloak
    x-show="isOpen"
    role="menu"
    x-bind:id="menuId"
    x-bind:aria-labelledby="triggerButtonId"
  >
    <template x-for="(menuItem, menuIndex) in menuItems" x-bind:key="menuIndex">
      <button
        type="button"
        role="menuitem"
        x-bind:id="menuItemIds[menuIndex]"
        x-bind:style="`padding-left: ${1 + menuItem.itemDepth * 0.75}rem`"
        x-on:click="selectItem(menuItem)"
        x-text="menuItem.label"
      ></button>
    </template>
  </div>
</div>

Common Usage Patterns

Scoped Action Events

huxDropdown({
  triggerId: 'options-menu',
  menuItems: [{ label: 'Copy Page URL', action: 'copy-page-url' }],
})
<div
  x-data="huxDropdown({ triggerId: 'options-menu', menuItems: [{ label: 'Copy Page URL', action: 'copy-page-url' }] })"
  x-on:hux-dropdown:options-menu:copy-page-url.window="navigator.clipboard.writeText(location.href).catch(() => {})"
>
  ...
</div>

Internal, External, and Action Items

huxDropdown({
  menuItems: [
    { label: 'Docs', href: '/patterns/dropdown' },
    { label: 'GitHub', href: 'https://github.com/markmead/hyperux', target: '_blank' },
    { label: 'Copy Page URL', action: 'copy-page-url' },
  ],
})
huxDropdown({
  triggerId: 'options-menu',
  menuItems: [
    {
      label: 'API',
      href: '#api',
      children: [{ label: 'huxDropdown(config)', href: '#huxdropdownconfig' }],
    },
    { label: 'Options', href: '#options' },
    { label: 'Quick Start', href: '#quick-start' },
    {
      label: 'Common Usage Patterns',
      href: '#common-usage-patterns',
      children: [
        { label: 'Scoped Action Events', href: '#scoped-action-events' },
        {
          label: 'Internal, External, and Action Items',
          href: '#internal-external-and-action-items',
        },
        { label: 'Nested Heading Structure Links', href: '#nested-heading-structure-links' },
      ],
    },
    { label: 'Behavior Contract', href: '#behavior-contract' },
    { label: 'Error Handling', href: '#error-handling' },
    { label: 'Accessibility Notes', href: '#accessibility-notes' },
    { label: 'Notes', href: '#notes' },
  ],
})

When you need to classify an item after lookup, resolve it first and then branch on the returned object in the same style as huxCommandPalette:

const menuItem = this.findMenuItem('GitHub')

if (menuItem?.target === '_blank') {
  // external link
}

Behavior Contract

Error Handling

Accessibility Notes

Notes