Skip to content
Home Theme Gallery

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

Menu
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.

A context menu with a visible trigger 375 px wide
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
Attributes
NameTypeDefaultDescription
openbooleanfalseReflected 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
Properties
NameTypeDefaultDescription
openbooleanfalseReflects to the open attribute.
placementOverlayPlacement'bottom'Reflects to the placement attribute.
alignDropdownAlign'start'Reflects to the align attribute.
triggerDropdownTrigger'click'Reflects to the trigger attribute.
Methods2
Methods
NameTypeDescription
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
Events
NameDescription
hui-openThe menu entered the top layer. It bubbles and is composed, and carries no detail.
hui-closeThe menu left the top layer. It bubbles and is composed, and carries no detail.
hui-selectAn 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
Slots
NameDescription
triggerThe 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
CSS custom properties
NameDefaultDescription
--hui-popoveroklch(1 0 0)Panel fill, drawn at 70 percent (TASK-054).
--hui-popover-foregroundoklch(0.145 0.008 326)Panel text colour.
--hui-foregroundoklch(0.145 0.008 326)Panel ring (10 percent) and separator (5 percent) colour.
--hui-radius-lg0.45remPanel radius.
--hui-space-xs0.25remPanel padding and separator margin.
::part() hooks2
::part() hooks
NameDescription
anchorThe span carrying the CSS anchor-name the panel points at.
panelThe 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.

Context menu
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.

Checkable and radio items
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 own menuitem role, 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 menuitemcheckbox or menuitemradio toggles its aria-checked and keeps the menu open; a plain menuitem closes it.
  • Disabled items are marked with disabled or aria-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 no contextmenu for a long press, so every action in one needs a visible route too, as “On a phone” above shows.

Keyboard

Keyboard
KeysAction
Arrow Down, Arrow UpMove to the next or previous enabled item, wrapping. Disabled items are skipped rather than focused.
Home, EndMove to the first or last item.
Enter, SpaceActivate the focused item and report hui-select.
Escape, Arrow LeftClose the menu and return focus to the trigger.
Printable charactersType-ahead jumps to the first label starting with the typed buffer, which resets after a pause.

Gotchas

None recorded.