Dropdown menu
A list of actions or options that opens from a button: every More menu, row menu and View option. It acts on something already on screen.
Playground
Change the controls to try each item prop. Press Actions to open the menu; the controls change its first item, and it stays open while you change them. The code under the preview lists only the props you changed.
import { DropdownMenuItem } from '@/ui/dropdown-menu';<DropdownMenuItem> Archive</DropdownMenuItem>Actions
The Halcyon Bank row's More menu. The trigger shows pressed while the menu is open. Each item starts with a 16px icon and ends with its shortcut; Delete goes last, after a separator, in red.
import { ArchiveIcon, CopyIcon, DotsThreeIcon, PencilSimpleIcon, TrashIcon } from '@phosphor-icons/react';
import { IconButton } from '@/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/ui/dropdown-menu';
export default function DropdownMenuDefault() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<IconButton label="More actions for Halcyon Bank" variant="secondary">
<DotsThreeIcon />
</IconButton>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem shortcut={['R']}>
<PencilSimpleIcon />
Rename
</DropdownMenuItem>
<DropdownMenuItem shortcut={['Mod', 'D']}>
<CopyIcon />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem shortcut={['E']}>
<ArchiveIcon />
Archive
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<TrashIcon />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Item states
Hover and arrow-key focus share the neutral fill. A disabled item stays in place at 50% so the menu doesn't shift. A description adds a second line for an action whose effect isn't clear from its name.
import { CopyIcon, PauseIcon, TrashIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/ui/dropdown-menu';
export default function DropdownMenuStates() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button>Rule actions</Button>} />
<DropdownMenuContent className="w-65">
<DropdownMenuItem>
<CopyIcon />
Copy email
</DropdownMenuItem>
<DropdownMenuItem disabled>
<CopyIcon />
Copy phone
</DropdownMenuItem>
<DropdownMenuItem description="Flo stops drafting replies until you turn it back on.">
<PauseIcon />
Pause this rule
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<TrashIcon />
Delete rule
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Choices
Radio items pick one value; checkbox items toggle and keep the menu open. The chosen item takes the selected fill and a brand check, and unchosen items keep the check slot empty so the labels line up. A 12px label names each group.
import { useState } from 'react';
import { Button } from '@/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/ui/dropdown-menu';
export default function DropdownMenuChoices() {
const [sort, setSort] = useState('updated');
const [archived, setArchived] = useState(false);
const [closed, setClosed] = useState(true);
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button>View options</Button>} />
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
<DropdownMenuRadioItem value="updated">Last updated</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name">Name</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="created">Created</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Show</DropdownMenuLabel>
<DropdownMenuCheckboxItem checked={archived} onCheckedChange={setArchived}>
Archived jobs
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={closed} onCheckedChange={setClosed}>
Closed applications
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}Submenu
A 12px caret marks an item that opens a submenu. It opens on hover or Right arrow and closes on Left arrow. One level deep only.
import { ArrowsLeftRightIcon, PencilSimpleIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/ui/dropdown-menu';
// Each stage's dot takes its phase colour (Decided 1).
const stages = [
{ name: 'Interview', dot: 'bg-text-subtle' },
{ name: 'CV sent', dot: 'bg-information' },
{ name: 'Offer', dot: 'bg-discovery' },
{ name: 'Placed', dot: 'bg-success' },
];
export default function DropdownMenuSubmenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button>Sara Okafor</Button>} />
<DropdownMenuContent>
<DropdownMenuItem>
<PencilSimpleIcon />
Edit
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<ArrowsLeftRightIcon />
Move to stage
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{stages.map((stage) => (
<DropdownMenuItem key={stage.name}>
<span aria-hidden className={`size-100 rounded-small ${stage.dot}`} />
{stage.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
}When things go wrong
A long label or name wraps inside the 320px panel; it never runs off the edge. An action the person can't take stays in place, disabled, with the reason as its description: Merge needs a second person, and Delete needs an Owner or Admin. The menu keeps its shape between records and people. When an action fails, an error toast says what happened and what to do next, and stays until closed. Items act once: the menu closes on the first press, so a double-click or a held Enter can't run the action twice.
import { ArchiveIcon, ArrowsMergeIcon, PaperPlaneTiltIcon, TrashIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/ui/dropdown-menu';
import { toast } from '@/ui/toast';
// A long label wraps inside the 320px panel. An action the person can't take is disabled with the reason as its
// description, so the menu keeps its shape between people. A failed action says so in a toast that stays.
export default function DropdownMenuUnhappy() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button>Nguyễn Thị Minh Khai</Button>} />
<DropdownMenuContent>
<DropdownMenuItem>
<PaperPlaneTiltIcon />
Send CV to Zoë O’Brien-Smith at Northbridge Talent Partners
</DropdownMenuItem>
<DropdownMenuItem disabled description="Select a second person to merge with.">
<ArrowsMergeIcon />
Merge
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
toast.add({ type: 'error', title: 'Couldn’t archive Nguyễn Thị Minh Khai', description: 'Check your connection and try again.' })
}
>
<ArchiveIcon />
Archive
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" disabled description="Only Owners and Admins can delete people.">
<TrashIcon />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Motion
The menu uses the Popup motion. Press Replay to close and open it again.
import { ArchiveIcon, PencilSimpleIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/ui/dropdown-menu';
// Replay closes the menu, then opens it again, so the 120ms exit and the 160ms entry run back to back.
export default function DropdownMenuMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 400);
};
return (
<div className="flex items-start gap-150">
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger render={<Button>Actions</Button>} />
<DropdownMenuContent>
<DropdownMenuItem>
<PencilSimpleIcon />
Rename
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
</div>
);
}Context menu
The row menu, opened with a right-click, a long press or Shift+F10. It holds the selection bar's actions for the rows it opened on, plus Open and Copy link for one row.
Kbd
A keyboard key, shown wherever a shortcut can be learned: tooltips, menus, the search field and the shortcuts sheet. One chip per key.