Skip to content

Actions & Overlays

Dropdown Menu

A button-triggered menu following the WAI-ARIA menu button pattern.

stableEnhancer: enhanceDropdownBuilt on <button popovertarget> + <ul popover>hydrateless/dropdown.css

A button-triggered menu following the WAI-ARIA menu button pattern, built on the native Popover API. Put popover on the menu and popovertarget on the trigger and the browser handles opening, Escape, light dismiss, and the top layer with no JavaScript. The enhancer adds role="menu" wiring, roving focus, arrow/Home/End/typeahead navigation that skips disabled items, menuitemcheckbox/menuitemradio state, and focus return to the trigger.

Dropdown

Open with a click or Down, then navigate with the arrow keys and typeahead. Esc closes and returns focus to the trigger; clicking outside closes without moving focus.

Live

Checkable items and groups

role="menuitemcheckbox" toggles aria-checked; role="menuitemradio" checks itself and unchecks its siblings in the same role="group". hl:select and onSelect report the new checked state. Pass closeOnSelect: false to keep the menu open while several boxes are toggled.

Live

JavaScript

This component ships an optional enhancer. With auto-init it loads automatically; to wire it yourself, import it directly:

import { enhanceDropdown } from '@hydrateless/enhancers/dropdown';

const handle = enhanceDropdown(document);
// handle.api  -> imperative controls
// handle.destroy()  -> tear everything down

Props

The framework bindings expose these props (and forward the rest to the root element).

PropTypeDefaultDescription
openbooleanNot setControlled open state; pair with `onOpenChange` (Vue: `v-model:open`, Svelte: `bind:open`).
defaultOpenbooleanfalseOpen on mount.
onOpenChange(open: boolean) => voidNot setCalled after the menu opens or closes.
onSelect(value: string, item: HTMLElement, checked?: boolean) => voidNot setCalled with the activated item value. For checkable items, `checked` is the new state.
closeOnSelectbooleantrueClose the menu after an item is activated.
placementPlacement'bottom-start'Placement used by the JS positioning fallback when CSS anchor positioning is missing.
DropdownItem.valuestringNot setValue reported by `onSelect`; defaults to the item text.
DropdownItem.role'menuitem' | 'menuitemcheckbox' | 'menuitemradio''menuitem'Plain action, toggle, or single-select item.
DropdownItem.checkedbooleanfalseInitial `aria-checked` for checkable items.
DropdownItem.disabledbooleanfalseSkipped by arrow navigation and typeahead; cannot be activated.
DropdownGroup.labelstringNot setAccessible name of the `role="group"`; scopes `menuitemradio` exclusivity.

Events and callbacks

Event / CallbackPayloadDescription
hl:open-change{ open: boolean }Fires after the menu opens or closes (also the `onOpenChange` callback).
hl:select{ value: string, item: HTMLElement, checked?: boolean }Cancelable CustomEvent carrying the item's `data-hl-value` (or text). `preventDefault()` vetoes the activation and restores the previous checked state.

CSS variables

Override these on any ancestor to theme the component. See the theme studio to preview changes live.

CSS variableUsed for
--hl-surfaceMenu background.
--hl-borderMenu border.
--hl-overlay-shadowMenu elevation.

Accessibility

  • The trigger exposes aria-haspopup="menu", aria-expanded, and aria-controls; the list is role="menu" labelled by the trigger.
  • Arrow keys, Home/End, typeahead, Enter/Space, and Esc are handled; disabled items are skipped and inert.
  • Focus returns to the trigger when the menu closes via Esc, Tab, or item activation, but not on light dismiss.
  • Every menuitemcheckbox/menuitemradio carries an explicit aria-checked.

Released under the MIT License.