Dropdown Menu
A dropdown menu triggered by a button that reveals a list of actions or options. This component combines a toggle button with a menu panel, providing a compact way to present multiple actions or choices without taking up permanent screen space. Common use cases include action menus, settings menus, context-sensitive options, and navigation.
The component manages its own focus lifecycle: when the menu opens, focus moves automatically to the first menu item. Arrow key navigation cycles through items with wrapping, and Escape closes the menu. The trigger button communicates its state to assistive technology via aria-haspopup and aria-expanded.
Implementation Notes
- Renders a wrapper
<div>containing a<button>trigger and a conditionally rendered menu<div> - Supports two-way binding on the
openprop with the parent - Uses reactive state to track the
menuRefDOM reference for querying menu items - Uses
$effect()to auto-focus the first[role="menuitem"]element when the menu opens - Menu items are discovered dynamically via
querySelectorAll("[role='menuitem']") - ArrowDown/ArrowUp navigation wraps around from last to first and first to last
- Home and End keys jump to first and last menu items respectively
- Escape key closes the menu
- The trigger button displays the
labeltext and uses it asaria-label - Spreads
restPropsonto the outer wrapper<div>
Props
label: string (required) -- accessible name for the menu button, also displayed as button textopen: boolean (default: false) -- whether the menu is visible; bindablechildren: slot (required) -- menu items, each should haverole="menuitem"andtabindex="-1"
Usage
"More actions" dropdown on a card component:
<DropdownMenu label="More actions" open={menuOpen}>
<MenuItem onclick={() => handleEdit()}>Edit</MenuItem>
<MenuItem onclick={() => handleDuplicate()}>Duplicate</MenuItem>
<MenuItem onclick={() => handleDelete()}>Delete</MenuItem>
</DropdownMenu>
Settings dropdown with dynamic items:
<DropdownMenu label="Settings" open={settingsOpen}>
{#each settingsOptions as option}
<MenuItem onclick={() => handleOption(option)}>
{option.label}
</MenuItem>
{/each}
</DropdownMenu>
Keyboard Interactions
- Enter or Space (on button): toggles the menu open/closed
- ArrowDown: moves focus to the next menu item, wraps from last to first
- ArrowUp: moves focus to the previous menu item, wraps from first to last
- Home: moves focus to the first menu item
- End: moves focus to the last menu item
- Escape: closes the menu
ARIA
aria-haspopup="true"-- on the trigger button, indicates it opens a menuaria-expanded="{open}"-- on the trigger button, communicates whether the menu is currently shownaria-label="{label}"-- on both the trigger button and the menu, provides an accessible namerole="menu"-- on the menu container, identifies it as a menu widgetrole="menuitem"-- expected on each child item provided by the consumer
When to Use
- Use for a menu that opens below a trigger button, such as a "More actions" button.
- Use to present a list of actions or options when screen space is limited.
- Use for settings menus, action menus, or context-sensitive options on cards and rows.
- Use when the menu should appear on click rather than right-click.
When Not to Use
- Do not use for form selections -- use Select or Combobox instead.
- Do not use for navigation -- use NavigationMenu instead.
- Do not use when the menu should appear on right-click -- use ContextMenu instead.
Headless
This headless component provides a trigger <button> with aria-haspopup and aria-expanded, a <div> menu container with role="menu", automatic focus management (first item on open), full arrow key navigation with wrapping, Home/End support, and Escape dismissal. The consumer provides all visual styling, positioning, animation, and menu item content.
Styles
The consumer provides all CSS styling. The component renders with a .dropdown-menu class for targeting. No default styles are included — this is a fully headless component.
Testing
- Verify the component renders a
<div>element with classdropdown-menu - Verify aria-label="{label}"` -- on both the trigger button and the menu, provides an accessible name
- Verify role="menu"` -- on the menu container, identifies it as a menu widget
- Verify role="menuitem"` -- expected on each child item provided by the consumer
- Verify keyboard interactions work correctly
- Verify pass-through attributes are applied
Advice
- Designers: Position the menu directly below the trigger button. Keep the menu width consistent and avoid overly long item labels. Use separators for grouping related actions.
- Developers: Ensure each child element has
role="menuitem"andtabindex="-1". Return focus to the trigger button after the menu closes.
Related components
menu-item— one item in a menu
References
- WAI-ARIA Menu Button Pattern: https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/
- WAI-ARIA menu role: https://www.w3.org/TR/wai-aria-1.2/#menu
- WAI-ARIA menuitem role: https://www.w3.org/TR/wai-aria-1.2/#menuitem