Skip to content

DropdownMenu

A menu of actions behind a button: the actions of a table row, a “More” menu. Its entries are MenuItems of the menu variant, links or buttons.

import { DropdownMenu } from '@zitdevs/ui';

The demo is server HTML with no React on the page, which runs one script, enhanceDropdownMenus(document) from @zitdevs/ui/enhance. Focus the button and press Enter: the first entry takes focus. Move with the arrow keys, Home and End, or type the start of a name; the disabled entry is skipped. Escape, or choosing an entry, closes the menu with focus back on the button.

<DropdownMenu id="row-1-menu" label="Actions for Ada Lovelace" triggerLabel="Actions" align="end">
<MenuItem variant="menu" href="/people/1/edit" title="Edit" />
<MenuItem variant="menu" type="submit" form="end-1" title="End" />
<MenuItem variant="menu" title="Remove" disabled />
</DropdownMenu>
Prop Type Default Notes
id string required The list’s id; the button names it with popovertarget. The button’s is <id>-trigger.
label string required Names the menu and the button. Say whose actions: “Actions for Ada Lovelace”.
triggerLabel string label The button’s visible text when shorter. It must be part of label (WCAG 2.5.3).
align 'start' | 'end' 'start' The button’s edge the list lines up with. end for a button at the end of a row.
children ReactNode required The entries: MenuItem variant="menu".
ref Ref<HTMLDivElement> — The wrapper around the button and the list.
anything else <div> attributes —

Exported types: DropdownMenuProps. From @zitdevs/ui/enhance: enhanceDropdownMenus.

  • The button has aria-haspopup="menu" and aria-expanded, which the enhancer keeps in step with the list; the list is a role="menu" named by the button.
  • Enter or Space opens it with focus on the first entry; ArrowDown opens at the first entry and ArrowUp at the last.
  • In the menu: ArrowDown and ArrowUp move (wrapping), Home and End jump, and typing the start of a name (or one letter, repeatedly) finds an entry. Entries that are aria-disabled are skipped.
  • Escape and Tab close it and leave focus on the button; choosing an entry does the same, so focus is never lost with the list. A press outside closes it and leaves focus where it went.
  • Every action is reachable from the keyboard; nothing exists only on hover.

The button and the list are native: a popovertarget button opens a popover list, and the browser closes it on Escape and on a press outside. The list opens beside the button, its entries in the tab order, and every link and button works. Browsers older than 2024 lack popovers: show the actions another way there. What the enhancer adds:

  • focus in the first entry on open, and the arrow keys, Home, End and type-ahead;
  • aria-expanded on the button, and focus back on it when the menu closes;
  • the list placed under the button (or above it, or lined up with its end edge).

In React the menu’s small client part runs the enhancer once it has hydrated. Without React, call enhanceDropdownMenus(document) once; it finds every [data-dropdown-menu].

surface-raised and border-default for the list, shadow-lg for its lift, primary-soft for the hovered entry, ink for text, the focus ring. Nothing is hard-coded.