DropdownMenu
Dropdown menus with menu / menuitem semantics, arrow-key and Home / End navigation, typeahead and managed focus. Supports plain, checkbox, radio, grouped and nested items, plus icons and shortcut hints.
Installation
Add component
pnpm dlx @elyri/cli@latest add dropdown-menuExamples
Basic
A group label, selectable items, a separator and a disabled item; the selection shows below.
Selected: Nothing yet
import { useState } from 'react';
import { DropdownMenu } from './components/elyri/DropdownMenu';
export function Example() {
const [selected, setSelected] = useState('Nothing yet');
return (
<div className="demo-ui-row">
<DropdownMenu>
<DropdownMenu.Trigger>Actions</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Edit</DropdownMenu.Label>
<DropdownMenu.Item onSelect={() => setSelected('Duplicate')}>Duplicate</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => setSelected('Rename')}>Rename</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => setSelected('Archive')}>Archive</DropdownMenu.Item>
<DropdownMenu.Item disabled>Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
<span className="demo-ui-label">Selected: {selected}</span>
</div>
);
}Checkbox and radio
CheckboxItem and RadioGroup / RadioItem stay open after toggling, so you can pick several in a row.
Sort: name · Compact: on
import { useState } from 'react';
import { DropdownMenu } from './components/elyri/DropdownMenu';
export function Example() {
const [sort, setSort] = useState('name');
const [compact, setCompact] = useState(true);
return (
<div className="demo-ui-row">
<DropdownMenu>
<DropdownMenu.Trigger>View</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Sort by</DropdownMenu.Label>
<DropdownMenu.RadioGroup value={sort} onValueChange={setSort}>
<DropdownMenu.RadioItem value="name">Name</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="date">Date modified</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem checked={compact} onCheckedChange={setCompact}>
Compact density
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu>
<span className="demo-ui-label">Sort: {sort} · Compact: {compact ? 'on' : 'off'}</span>
</div>
);
}Submenu
Sub / SubTrigger / SubContent nest to any depth; open with arrow keys or hover, or keep an item open.
import { DropdownMenu } from './components/elyri/DropdownMenu';
export function Example() {
return (
<div className="demo-ui-row">
<DropdownMenu>
<DropdownMenu.Trigger>Share</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>Copy link</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Invite people</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Email</DropdownMenu.Item>
<DropdownMenu.Item>Slack</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>More…</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Teams</DropdownMenu.Item>
<DropdownMenu.Item>Discord</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
{/* onSelect 里 preventDefault 让菜单保持打开 */}
<DropdownMenu.Item onSelect={(event) => event.preventDefault()} danger>
Revoke access
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</div>
);
}Icons, shortcuts and asChild
Items take a leading icon and a trailing shortcut hint; pass asChild to reuse a Button as the trigger.
Selected: Nothing yet
import { useState } from 'react';
import { Button } from './components/elyri/Button';
import { DropdownMenu } from './components/elyri/DropdownMenu';
export function Example() {
const [selected, setSelected] = useState('Nothing yet');
const pick = (label: string) => () => setSelected(label);
return (
<div className="demo-ui-row">
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<Button variant="secondary">File</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={pick('New')}>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
New
<DropdownMenu.Shortcut>⌘N</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={pick('Save')}>
Save
<DropdownMenu.Shortcut>⌘S</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item danger onSelect={pick('Delete')}>
Delete
<DropdownMenu.Shortcut>⌫</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
<span className="demo-ui-label">Selected: {selected}</span>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Initial open state when uncontrolled |
onOpenChange | (open: boolean) => void | — | Called whenever the open state changes |
placement | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Preferred side: top / bottom / left / right |
align | 'start' | 'center' | 'end' | 'start' | Cross-axis alignment: start / center / end |
offset | number | 4 | Distance from the trigger |
DropdownMenu.Trigger | ButtonHTMLAttributes | — | Trigger element; arrow keys open the menu too, asChild merges onto the only child |
Trigger.asChild | boolean | false | Skip the built-in button and merge behaviour onto the only child |
DropdownMenu.Content | HTMLAttributes | — | The menu container, role="menu" |
DropdownMenu.Item | ButtonHTMLAttributes | — | A menu item, role="menuitem", closes the menu on select |
onSelect | (event) => void | — | Called on select; call event.preventDefault() to keep the menu open |
disabled | boolean | — | Disabled item, skipped by keyboard navigation |
danger | boolean | — | Destructive actions such as delete render in the danger colour |
DropdownMenu.CheckboxItem | ButtonHTMLAttributes | — | A checkable item, role="menuitemcheckbox", stays open after toggling |
CheckboxItem.checked | boolean | — | Controlled checked state |
CheckboxItem.defaultChecked | boolean | false | Initial checked state when uncontrolled |
CheckboxItem.onCheckedChange | (checked: boolean) => void | — | Called whenever the checked state changes |
DropdownMenu.RadioGroup | HTMLAttributes | — | A group of radio items, role="group" |
RadioGroup.value | string | — | Controlled selected value |
RadioGroup.defaultValue | string | — | Initial selected value when uncontrolled |
RadioGroup.onValueChange | (value: string) => void | — | Called whenever the selected value changes |
DropdownMenu.RadioItem | ButtonHTMLAttributes | — | A radio item, role="menuitemradio", stays open after selecting |
RadioItem.value | string | — | This item value, matched against the RadioGroup value |
DropdownMenu.Sub | object | — | Submenu root, manages its own open state |
DropdownMenu.SubTrigger | ButtonHTMLAttributes | — | The submenu trigger, aria-haspopup, opens on hover or arrow keys |
DropdownMenu.SubContent | HTMLAttributes | — | The submenu container, opens to the right by default |
DropdownMenu.Group | HTMLAttributes | — | A group container, role="group", accepts an aria-label |
DropdownMenu.Label | HTMLAttributes | — | A group label |
DropdownMenu.Separator | HTMLAttributes | — | A divider between groups |
DropdownMenu.Shortcut | HTMLAttributes | — | A trailing shortcut hint such as ⌘K |