DropdownMenu
Keyboard-navigable action menu anchored to a trigger.
import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuItem } from '@elirobinson/react/components/organisms/DropdownMenu';Styles: @elirobinson/react/styles/organisms/DropdownMenu.css — already included when you import @elirobinson/react/styles.css.
Show code
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@elirobinson/react/components/organisms/DropdownMenu';
export default function Basic() {
return (
<DropdownMenu>
<DropdownMenuTrigger className="ds-button ds-button--secondary">
Guide actions
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>U8 Soccer Season Plan</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}When to use it
Use DropdownMenu for a set of actions anchored to a trigger — row actions in a table, a "…"
overflow menu, a view switcher. It's action-oriented: items are role="menuitem" and run a
handler, they don't navigate. If every item should actually change the URL, use NavigationMenu
or plain links instead — a role="menuitem" on a link tells assistive tech the wrong thing
about what activating it does.
Aligning items with inset
inset on DropdownMenuLabel/DropdownMenuItem adds left padding so plain text lines up with
sibling items that have a leading marker — a checkmark on the current selection, in this case.
Show code
import { useState } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@elirobinson/react/components/organisms/DropdownMenu';
const views = ['List', 'Grid', 'Board'] as const;
export default function Alignment() {
const [view, setView] = useState<(typeof views)[number]>('List');
return (
<DropdownMenu>
<DropdownMenuTrigger className="ds-button ds-button--secondary">
View: {view}
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel inset>Layout</DropdownMenuLabel>
{views.map((option) => (
<DropdownMenuItem key={option} inset={option !== view} onClick={() => setView(option)}>
{option === view ? `✓ ${option}` : option}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | — |
onOpenChange | ((open: boolean) => void) | — | — |
open | boolean | — | — |
DropdownMenuContent
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
DropdownMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — | — |
Also accepts all HTMLAttributes<HTMLDivElement> props.
DropdownMenuSeparator
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
DropdownMenuTrigger
No props of its own beyond the inherited HTML attributes.
Also accepts all ButtonHTMLAttributes<HTMLButtonElement> props.
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — | — |
Also accepts all ButtonHTMLAttributes<HTMLButtonElement> props.
Accessibility
DropdownMenuContentrendersrole="menu", portaled todocument.body; eachDropdownMenuItemisrole="menuitem". The trigger carriesaria-haspopup="menu",aria-expanded, andaria-controls.Escapecloses the menu from anywhere on the page — a document-level listener, active only while the menu is open. Clicking anywhere outside the trigger or the menu content also closes it.- Opening the menu does not move focus into it — clicking or activating the trigger leaves DOM focus on the trigger button itself. Arrow-key/Home/End navigation only takes over once a menu item already has focus (for example, after tabbing to one).
- Keyboard, once a menu item has focus:
ArrowDown/ArrowUpmove focus between items and wrap around at both ends (ArrowDownpast the last item goes to the first, and vice versa — unlike Combobox/CommandPalette's clamped, non-wrapping model, since this menu moves real DOM focus rather than tracking anaria-activedescendant).Home/Endjump to the first/last item.Tabcloses the menu and lets focus continue to whatever's next in the page, rather than trapping it. DropdownMenuSeparatorrendersrole="separator"witharia-orientation="horizontal".- Content is positioned with fixed coordinates computed from the trigger's bounding rect, recalculated when the menu opens (not on every scroll or resize).
Do
- Group related items under a DropdownMenuLabel and separate groups with DropdownMenuSeparator.
- Keep item labels as short verb phrases — the menu is for actions, not longform text.
- Give the trigger real content (icon and/or label) — aria-haspopup and aria-expanded are already wired.
- Test with Tab specifically — since initial focus stays on the trigger, keyboard flows depend on Tab order reaching an item.
Don't
- Put a link where an action belongs — items are buttons that run a handler, not anchors that navigate.
- Rely on hover to open it — activation is click/keyboard only, with no hover-intent timer.
- Put a destructive action next to routine ones without a separator.
- Assume the first item gets focus automatically on open — it doesn’t.