Skip to content
HHydro UIdocs
Navigate
ComponentsDropdown Menu
ImplementedOverlay / navigation

Dropdown Menu

Present a short command or selection list from a menu button.

Interactive

Try it

open, loop, close-on-select, placement
AccountProfileSettingsSign out
Live previewReady
MarkupStateEvents
<hui-menu id="playground-menu" aria-label="Account actions" placement="bottom" loop>
  <hui-menu-trigger>Account</hui-menu-trigger>
  <hui-menu-content>
    <hui-menu-item value="profile">Profile</hui-menu-item>
    <hui-menu-item value="settings">Settings</hui-menu-item>
    <hui-menu-separator></hui-menu-separator>
    <hui-menu-item value="sign-out">Sign out</hui-menu-item>
  </hui-menu-content>
</hui-menu>
data-state
Waiting for enhancement

No component events yet.

Usage

Use hui-menu for a short command or selection list opened from a trigger. Use a select for choosing a form value and a popover for arbitrary interactive content. Do not use a menu for long navigational lists or unrelated page content.

Anatomy

  • Root: hui-menu
  • Trigger and popup: hui-menu-trigger, hui-menu-content
  • Items: hui-menu-item, hui-menu-item-checkbox, hui-menu-item-radio
  • Supporting parts: hui-menu-group, hui-menu-label, hui-menu-separator

Examples

Fixture exampleSource-backed
<section aria-labelledby="menu-heading">
    <h2 id="menu-heading">Account actions</h2>
    <hui-menu id="account-menu" aria-label="Account actions" loop close-on-select>
      <hui-menu-trigger><b>Account</b></hui-menu-trigger>
      <hui-menu-content>
        <hui-menu-item value="profile">Profile</hui-menu-item>
        <hui-menu-item-checkbox value="compact">Compact mode</hui-menu-item-checkbox>
        <hui-menu-separator></hui-menu-separator>
        <hui-menu-group>
          <hui-menu-label>Theme</hui-menu-label>
          <hui-menu-item-radio value="light" checked>Light</hui-menu-item-radio>
          <hui-menu-item-radio value="dark">Dark</hui-menu-item-radio>
        </hui-menu-group>
        <hui-menu-item value="disabled" disabled>Disabled action</hui-menu-item>
      </hui-menu-content>
    </hui-menu>
  </section>

API reference

hui-menu

AttributeMeaning
openBoolean source of truth for visibility.
default-openSeeds open on first enhancement; later changes are inert.
loopAllows ArrowUp/ArrowDown to wrap.
close-on-selectCloses after activating any menu item. Checkbox and radio items remain open by default.
placementPreferred top, bottom, left, or right placement. Collision fallback is exposed as data-placement.
dirDirection used by the navigation adapter.
idStable base for content and item IDs. ID-less menus omit ARIA references.
aria-label, aria-labelledby, aria-describedbyInherited by the menu content when it does not provide its own value.

The root exposes data-state="open|closed" and emits hui-open-change with { open }.

Trigger and content

hui-menu-trigger renders a native button with aria-haspopup="menu", aria-expanded, and guarded aria-controls. Its disabled and accessible-name attributes are applied to the button.

hui-menu-content renders role="menu", aria-orientation="vertical", and an inline hidden attribute while closed. Open content is moved to a document-level portal so clipped ancestors cannot hide it. The content exposes data-state and data-placement. After opening, page scrolling does not reposition the content; it stays at its last document coordinates, allowing the page to carry it out of the viewport. Opening and viewport resizing still run collision-aware positioning.

During a motion-safe exit, the content keeps its mount and portal until the tokenized transition completes. It is aria-hidden, inert, and non-interactive while data-state="closed"; no-transition and reduced-motion paths apply hidden immediately.

Items

  • hui-menu-item renders role="menuitem".
  • hui-menu-item-checkbox renders role="menuitemcheckbox" and uses the boolean checked attribute as its source of truth.
  • hui-menu-item-radio renders role="menuitemradio" and uses the boolean checked attribute. Activating one radio clears the other radio items in the same menu.
  • All item types support value, disabled, and highlighted.
  • When value is omitted, item identity falls back to the host id, then the item's positional index. Use explicit values for collections whose order can change.
  • The highlighted item exposes data-highlighted and is the only item with tabindex="0"; other enabled items use tabindex="-1".
  • Checkbox and radio items expose aria-checked and data-state.

Activating an item emits hui-select with { value }. Checkbox and radio state changes also emit hui-checked-change with { value, checked } from the menu root.

hui-menu-group renders role="group" and uses a contained hui-menu-label for aria-labelledby when no explicit group label is provided. hui-menu-separator renders role="separator".

States and styling

StateContract and styling guidance
DefaultClosed menu with the trigger available in the page tab order.
HoverHighlight an enabled item with --hui-color-interactive-hover.
ActiveUse --hui-color-interactive-active for pressed trigger or item feedback.
FocusKeep the trigger or roving item focus ring visible with --hui-color-focus-ring.
DisabledDisabled triggers/items use --hui-color-interactive-disabled and cannot activate.
ErrorMenu has no validation state; use a field or command result to communicate errors.
State channel

Attributes are the source of truth. Style derived state with data-state, ARIA attributes, and the --hui-* token aliases.

Accessibility and keyboard support

  • Trigger ArrowDown opens and focuses the first enabled item.
  • Trigger ArrowUp opens and focuses the last enabled item.
  • Trigger Enter, Space, and click toggle the menu.
  • Menu ArrowUp/ArrowDown moves the roving tab stop, skipping disabled items.
  • Home and End move to the first and last enabled items.
  • Printable keys use a 500ms buffered typeahead search and skip disabled items.
  • Enter and Space activate the highlighted item.
  • Escape closes the menu and restores focus to the trigger.
  • Tab closes the menu while preserving the browser's normal focus movement.
  • Pointer activation works for all item kinds; disabled items do nothing.
  • Outside pointer-down and outside focus close the menu. The dismissable-layer stack closes only the topmost open layer.

SSR and forms

SSR renders the trigger ARIA relationship, menu roles, item states, and closed content with hidden="hidden". The server never emits portal markers. IDs are pure functions of the root ID and stable item values; id-less menus do not emit aria-controls or generated IDs. default-open serializes visible content.

Hydration re-renders component-owned template nodes while preserving projected consumer content and host attributes. The client portal is applied only when an open menu is enhanced. Without JavaScript, the trigger remains a native button and the menu content remains hidden; consumers can use default-open for a progressive visible fallback.

Code

<hui-menu id="account-menu" aria-label="Account actions">
  <hui-menu-trigger>Account</hui-menu-trigger>
  <hui-menu-content>
    <hui-menu-item value="profile">Profile</hui-menu-item>
    <hui-menu-item value="sign-out">Sign out</hui-menu-item>
  </hui-menu-content>
</hui-menu>
<hui-menu id="playground-menu" aria-label="Account actions" placement="bottom" loop>
  <hui-menu-trigger>Account</hui-menu-trigger>
  <hui-menu-content>
    <hui-menu-item value="profile">Profile</hui-menu-item>
    <hui-menu-item value="settings">Settings</hui-menu-item>
    <hui-menu-separator></hui-menu-separator>
    <hui-menu-item value="sign-out">Sign out</hui-menu-item>
  </hui-menu-content>
</hui-menu>