Dropdown menu
The custom element is <hui-dropdown-menu>.
Overview
A popover with menu semantics: a single tab stop, arrow and type-ahead navigation, and selection. trigger="contextmenu" absorbs shadcn's Context Menu, opening the same menu on right-click.
Example
Source
<hui-dropdown-menu> <hui-button slot="trigger" variant="outline">Actions</hui-button> <button role="menuitem" data-value="edit">Edit</button> <button role="menuitem" data-value="duplicate">Duplicate</button> <div role="separator"></div> <button role="menuitem" data-value="delete">Delete</button></hui-dropdown-menu>The Go template that renders it:
<hui-dropdown-menu> <hui-button slot="trigger" variant="outline">Actions</hui-button> {{ range .Actions }} <button role="menuitem" data-value="{{ .Name }}">{{ .Label }}</button> {{ end }}</hui-dropdown-menu>On a phone
A context menu opens on contextmenu: a right click, or Shift+F10. iOS Safari fires no contextmenu for a long press, so on a phone the same actions need a visible trigger too. Give the row a More button that opens an identical menu.
Source
<div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--hui-border);border-radius:var(--hui-radius-lg)"> <hui-dropdown-menu trigger="contextmenu"> <span slot="trigger" style="font-size:.875rem">Quarterly report.pdf</span> <button role="menuitem" data-value="open">Open</button> <button role="menuitem" data-value="rename">Rename</button> <button role="menuitem" data-value="delete">Delete</button> </hui-dropdown-menu> <hui-dropdown-menu> <hui-button slot="trigger" variant="ghost" size="sm" aria-label="More actions for Quarterly report.pdf">More</hui-button> <button role="menuitem" data-value="open">Open</button> <button role="menuitem" data-value="rename">Rename</button> <button role="menuitem" data-value="delete">Delete</button> </hui-dropdown-menu></div>API
Attributes4
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Reflected open state, inherited from the popover base. |
placement | "top" | "bottom" | "left" | "right" | "bottom" | Which side of the trigger the menu prefers. An unknown value falls back to bottom. |
align | "start" | "end" | "start" | Which trigger edge the menu aligns to. end puts the panel's inline-end edge on the trigger's. An unknown value falls back to start. |
trigger | "click" | "contextmenu" | "click" | The gesture that opens the menu. contextmenu opens it on right-click and prevents the browser menu. |
Properties4
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Reflects to the open attribute. |
placement | OverlayPlacement | 'bottom' | Reflects to the placement attribute. |
align | DropdownAlign | 'start' | Reflects to the align attribute. |
trigger | DropdownTrigger | 'click' | Reflects to the trigger attribute. |
Methods2
| Name | Type | Description |
|---|---|---|
show(from?) | - | Opens the menu, focuses its first enabled item and records from as the element focus returns to. |
hide() | - | Closes the menu, clears the roving tab index and restores focus. |
Events4
| Name | Description |
|---|---|
hui-open | The menu entered the top layer. It bubbles and is composed, and carries no detail. |
hui-close | The menu left the top layer. It bubbles and is composed, and carries no detail. |
hui-select | An item was chosen. It bubbles and is composed, with detail { value, item } where value is data-value, the value attribute, or the item's trimmed text. |
click on [slot="trigger"] | Opens the menu when closed and closes it when open. A pointerdown while open is treated as the close gesture. |
Slots2
| Name | Description |
|---|---|
trigger | The element the menu is anchored to. |
(default) | The menu items: role="menuitem", menuitemcheckbox or menuitemradio, optionally wrapped in role="group" and separated by role="separator". |
CSS custom properties5
| Name | Default | Description |
|---|---|---|
--hui-popover | oklch(1 0 0) | Panel fill, drawn at 70 percent (TASK-054). |
--hui-popover-foreground | oklch(0.145 0.008 326) | Panel text colour. |
--hui-foreground | oklch(0.145 0.008 326) | Panel ring (10 percent) and separator (5 percent) colour. |
--hui-radius-lg | 0.45rem | Panel radius. |
--hui-space-xs | 0.25rem | Panel padding and separator margin. |
::part() hooks2
| Name | Description |
|---|---|
anchor | The span carrying the CSS anchor-name the panel points at. |
panel | The native popover panel, carrying role="menu". |
States
Context menu
trigger="contextmenu" opens the menu on a right click, or Shift+F10, and suppresses the browser's own menu. It opens as the pointer is released, and a second right click moves it rather than closing it. Touch has no equivalent gesture, which is what the phone example above is for.
Source
<hui-dropdown-menu trigger="contextmenu"> <div slot="trigger" style="padding:1rem;border:1px dashed var(--hui-border)">Right-click me</div> <button role="menuitem" data-value="copy">Copy</button></hui-dropdown-menu>Checkable and radio items
The two checkable roles side by side. menuitemcheckbox toggles on its own and shows a tick; menuitemradio unchecks its siblings and shows a dot. Both keep the menu open when chosen, and each set sits in a role="group" with its own heading.
Source
<hui-dropdown-menu> <hui-button slot="trigger" variant="outline">View</hui-button> <div role="group" aria-label="Appearance"> <span data-label>Appearance</span> <button role="menuitemcheckbox" data-value="status-bar" aria-checked="true">Status bar</button> <button role="menuitemcheckbox" data-value="activity-bar" aria-checked="false">Activity bar</button> <button role="menuitemcheckbox" data-value="panel" aria-checked="false">Panel</button> </div> <hr role="separator" /> <div role="group" aria-label="Layout"> <span data-label>Layout</span> <button role="menuitemradio" data-value="grid" aria-checked="true">Grid</button> <button role="menuitemradio" data-value="list" aria-checked="false">List</button> </div></hui-dropdown-menu>Server-side mechanics
The server owns the trigger and every menu item: slotted role="menuitem", menuitemcheckbox or menuitemradio elements, optionally grouped and separated, plus open, placement and trigger. Render trigger="contextmenu" or open to set the gesture or the initial state, and read those three reflected attributes back. It is not form-associated and submits nothing; a chosen item is reported only as a bubbling, composed hui-select event carrying { value, item }, which the host handles with Alpine or a document-level listener. Items are read from the slot on demand, so an hx-swap that replaces menu items while it is open is picked up on the next key or click, and a swap that replaces the whole menu closes it through disconnectedCallback(). A swap that replaces the trigger while the menu is open drops focus to <body> on close rather than restoring it to a node the swap removed.
Accessibility
- The panel carries
role="menu"; each slotted item carries its ownmenuitemrole, so the host owns the item markup. - The menu is one tab stop: exactly one item has
tabindex="0"and the rest-1, moved by the arrow keys. - Focus enters the first enabled item on open and returns to the trigger on close; leaving the menu with Tab closes it.
- A
menuitemcheckboxormenuitemradiotoggles itsaria-checkedand keeps the menu open; a plainmenuitemcloses it. - Disabled items are marked with
disabledoraria-disabled="true"and are never activated by pointer, keyboard or hover. - A context menu (
trigger="contextmenu") opens on a right click or Shift+F10. iOS Safari fires nocontextmenufor a long press, so every action in one needs a visible route too, as “On a phone” above shows.
Keyboard
| Keys | Action |
|---|---|
Arrow Down, Arrow Up | Move to the next or previous enabled item, wrapping. Disabled items are skipped rather than focused. |
Home, End | Move to the first or last item. |
Enter, Space | Activate the focused item and report hui-select. |
Escape, Arrow Left | Close the menu and return focus to the trigger. |
Printable characters | Type-ahead jumps to the first label starting with the typed buffer, which resets after a pause. |
Gotchas
None recorded.