Dropdown Menu
Present a short command or selection list from a menu button.
Try it
<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
<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
| Attribute | Meaning |
|---|---|
open | Boolean source of truth for visibility. |
default-open | Seeds open on first enhancement; later changes are inert. |
loop | Allows ArrowUp/ArrowDown to wrap. |
close-on-select | Closes after activating any menu item. Checkbox and radio items remain open by default. |
placement | Preferred top, bottom, left, or right placement. Collision fallback is exposed as data-placement. |
dir | Direction used by the navigation adapter. |
id | Stable base for content and item IDs. ID-less menus omit ARIA references. |
aria-label, aria-labelledby, aria-describedby | Inherited 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-itemrendersrole="menuitem".hui-menu-item-checkboxrendersrole="menuitemcheckbox"and uses the booleancheckedattribute as its source of truth.hui-menu-item-radiorendersrole="menuitemradio"and uses the booleancheckedattribute. Activating one radio clears the other radio items in the same menu.- All item types support
value,disabled, andhighlighted. - When
valueis omitted, item identity falls back to the hostid, then the item's positional index. Use explicit values for collections whose order can change. - The highlighted item exposes
data-highlightedand is the only item withtabindex="0"; other enabled items usetabindex="-1". - Checkbox and radio items expose
aria-checkedanddata-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
| State | Contract and styling guidance |
|---|---|
| Default | Closed menu with the trigger available in the page tab order. |
| Hover | Highlight an enabled item with --hui-color-interactive-hover. |
| Active | Use --hui-color-interactive-active for pressed trigger or item feedback. |
| Focus | Keep the trigger or roving item focus ring visible with --hui-color-focus-ring. |
| Disabled | Disabled triggers/items use --hui-color-interactive-disabled and cannot activate. |
| Error | Menu has no validation state; use a field or command result to communicate errors. |
Attributes are the source of truth. Style derived state with data-state, ARIA attributes, and the --hui-* token aliases.
Accessibility and keyboard support
- Trigger
ArrowDownopens and focuses the first enabled item. - Trigger
ArrowUpopens 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.
HomeandEndmove 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>