Dropdown Menu
A list of actions or options that appears from a trigger, keeping secondary commands one click away without cluttering the interface.
import {
ChevronsUpDownIcon,
CreditCardIcon,
KeyboardIcon,
LifeBuoyIcon,
LogOutIcon,
SettingsIcon,
UserIcon,
} from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" className="h-10 gap-2.5 pr-2.5 pl-1.5" />
}
>
<Avatar size="sm">
<AvatarFallback>AL</AvatarFallback>
</Avatar>
<span className="flex flex-col items-start leading-tight">
<span className="text-sm font-medium">Ada Lovelace</span>
</span>
<ChevronsUpDownIcon
data-icon="inline-end"
className="text-muted-foreground"
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-60">
<DropdownMenuGroup>
<DropdownMenuLabel>
<span className="block text-sm font-medium text-foreground">
Ada Lovelace
</span>
<span className="block font-normal">ada@analytical.engine</span>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserIcon />
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon />
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon />
Settings
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<KeyboardIcon />
Keyboard shortcuts
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<LifeBuoyIcon />
Support
</DropdownMenuItem>
<DropdownMenuItem>
<LogOutIcon />
Log out
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/dropdown-menu.jsonUsage
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
Options
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>My account</DropdownMenuLabel>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>The trigger renders any element through render — usually a Button. Items always live inside a DropdownMenuGroup, so screen readers announce the group label and separators have something to separate.
Examples
Account menu
The most common menu: who you are at the top, account actions in the middle, and the way out last. Shortcuts sit on the right edge in a quieter color so they're scannable without competing with labels.
import {
ChevronsUpDownIcon,
CreditCardIcon,
KeyboardIcon,
LifeBuoyIcon,
LogOutIcon,
SettingsIcon,
UserIcon,
} from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function DropdownMenuDemo() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" className="h-10 gap-2.5 pr-2.5 pl-1.5" />
}
>
<Avatar size="sm">
<AvatarFallback>AL</AvatarFallback>
</Avatar>
<span className="flex flex-col items-start leading-tight">
<span className="text-sm font-medium">Ada Lovelace</span>
</span>
<ChevronsUpDownIcon
data-icon="inline-end"
className="text-muted-foreground"
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-60">
<DropdownMenuGroup>
<DropdownMenuLabel>
<span className="block text-sm font-medium text-foreground">
Ada Lovelace
</span>
<span className="block font-normal">ada@analytical.engine</span>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserIcon />
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon />
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon />
Settings
<DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<KeyboardIcon />
Keyboard shortcuts
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<LifeBuoyIcon />
Support
</DropdownMenuItem>
<DropdownMenuItem>
<LogOutIcon />
Log out
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Checkbox items
Use DropdownMenuCheckboxItem for independent on/off settings such as visible columns. Checkbox items stay open when clicked (closeOnClick defaults to false), so people can toggle several in one visit.
"use client"
import * as React from "react"
import { Settings2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const columns = [
{ id: "status", label: "Status" },
{ id: "assignee", label: "Assignee" },
{ id: "priority", label: "Priority" },
{ id: "due", label: "Due date" },
{ id: "estimate", label: "Estimate" },
]
export default function DropdownMenuCheckboxes() {
const [visible, setVisible] = React.useState<Record<string, boolean>>({
status: true,
assignee: true,
priority: true,
due: false,
estimate: false,
})
const count = Object.values(visible).filter(Boolean).length
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
<Settings2Icon data-icon="inline-start" />
Columns
<span className="text-muted-foreground tabular-nums">{count}</span>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52">
<DropdownMenuGroup>
<DropdownMenuLabel>Visible columns</DropdownMenuLabel>
{columns.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={visible[column.id]}
onCheckedChange={(checked) =>
setVisible((current) => ({ ...current, [column.id]: checked }))
}
>
{column.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuCheckboxItem
checked={count === columns.length}
onCheckedChange={(checked) =>
setVisible(
Object.fromEntries(columns.map((c) => [c.id, checked]))
)
}
>
Show all
</DropdownMenuCheckboxItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Radio group
DropdownMenuRadioGroup holds mutually exclusive choices — sort order, density, a view mode. Reflect the current choice in the trigger's label so the state is visible while the menu is closed.
"use client"
import * as React from "react"
import { ArrowDownWideNarrowIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const sortOptions = {
updated: "Last updated",
created: "Date created",
name: "Name",
priority: "Priority",
}
type SortKey = keyof typeof sortOptions
export default function DropdownMenuRadioGroupDemo() {
const [sort, setSort] = React.useState<SortKey>("updated")
const [direction, setDirection] = React.useState("desc")
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
<ArrowDownWideNarrowIcon data-icon="inline-start" />
Sort: {sortOptions[sort]}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52">
<DropdownMenuGroup>
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={sort}
onValueChange={(value) => setSort(value as SortKey)}
>
{Object.entries(sortOptions).map(([value, label]) => (
<DropdownMenuRadioItem key={value} value={value}>
{label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Order</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={direction}
onValueChange={setDirection}
>
<DropdownMenuRadioItem value="asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="desc">
Descending
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Submenus
Nest related actions with DropdownMenuSub. Submenus open on hover after a short delay and with → from the keyboard. Keep nesting to one level — a second level is a sign the menu is doing a page's job.
import {
CopyIcon,
LinkIcon,
MailIcon,
MessageSquareIcon,
PlusIcon,
Share2Icon,
UserPlusIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function DropdownMenuSubmenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
<PlusIcon data-icon="inline-start" />
New
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuItem>
New document
<DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
New folder
<DropdownMenuShortcut>⇧⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon />
Duplicate
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserPlusIcon />
Invite people
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuGroup>
<DropdownMenuItem>
<MailIcon />
Email
</DropdownMenuItem>
<DropdownMenuItem>
<MessageSquareIcon />
Message
</DropdownMenuItem>
<DropdownMenuItem>
<LinkIcon />
Copy invite link
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Share2Icon />
Share
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuGroup>
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuItem>Export as PDF</DropdownMenuItem>
<DropdownMenuItem>Publish to web…</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Row actions
A ghost icon button with a ⋯ glyph is the conventional home for per-item actions in lists and tables. Align the menu to the trigger's end (align="end") so it opens toward the content, and give the trigger an aria-label that names the item it acts on.
import {
ArchiveIcon,
CopyIcon,
MoreHorizontalIcon,
PencilIcon,
Trash2Icon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function DropdownMenuRowActions() {
return (
<div className="flex w-full max-w-md items-center gap-3 rounded-xl border bg-card px-4 py-3 shadow-raised">
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">Quarterly planning</span>
<span className="text-body-xs text-muted-foreground">
Edited 2 hours ago
</span>
</div>
<Badge variant="success">Active</Badge>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label="Actions for Quarterly planning"
/>
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem>
<PencilIcon />
Rename
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon />
Duplicate
<DropdownMenuShortcut>⌘D</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Delete
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}Disabled items
Disable an item when the action exists but is temporarily unavailable, and say why in the label. A disabled item can't be activated, but the arrow keys still reach it, so screen reader users hear that the action exists. If an action never applies in the current context, remove it instead.
import { DownloadIcon, FileTextIcon, LockIcon, PrinterIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export default function DropdownMenuDisabled() {
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
<DownloadIcon data-icon="inline-start" />
Export
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuLabel>Export report as</DropdownMenuLabel>
<DropdownMenuItem>
<FileTextIcon />
CSV
</DropdownMenuItem>
<DropdownMenuItem>
<PrinterIcon />
PDF
</DropdownMenuItem>
<DropdownMenuItem disabled>
<LockIcon />
Excel — Pro plan
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}Guidelines
When to use
- To collect secondary actions behind a single trigger: row actions, overflow toolbars, account menus.
- To offer a small set of view settings — toggles and exclusive choices — without a settings page.
When not to use
- To choose a value that becomes part of a form — use a Select or Combobox.
- For the primary action of a view — keep it visible as a Button.
- For site or app navigation with rich panels — use a Navigation Menu.
- For actions on an area, discovered by right-click — use a Context Menu, and always offer the same actions somewhere visible.
Item content
Start every item with a verb or a noun that names the destination: Rename, Duplicate, Settings. Use sentence case, keep items to one line, and append an ellipsis (…) only when the item opens a dialog that asks for more input before anything happens.
Icons are optional, but all-or-nothing within a group: a column of labels where some have icons and some don't looks broken. Use inset on unaccompanied items when the rest of the menu has icons or indicators.
Destructive actions
Put destructive items last, separated from the rest, and use variant="destructive". If the action can't be undone, the item should open an Alert Dialog rather than act immediately. Never make a destructive item the first thing keyboard focus lands on.
Shortcuts
DropdownMenuShortcut is a hint, not a binding — register the actual keyboard handler elsewhere. Show shortcuts only for actions that really have one, use the platform's symbols (⌘ ⇧ ⌥ ⌃ ⌫ ↵) in their conventional order (⌃ ⌥ ⇧ ⌘), and don't show a shortcut for an item that is disabled.
Size and grouping
Seven to ten items is a comfortable maximum. Group related items and separate groups with DropdownMenuSeparator; label groups with DropdownMenuLabel when their purpose isn't obvious. A menu grows to fit its longest item and is never narrower than its trigger or 11rem (176px). To fix its width, set one on DropdownMenuContent, for example className="w-56".
Accessibility
Dropdown Menu follows the WAI-ARIA menu button pattern through Base UI. The trigger gets aria-haspopup and aria-expanded; items get the menuitem, menuitemcheckbox or menuitemradio role; focus moves into the menu on open and returns to the trigger on close.
| Key | Behavior |
|---|---|
EnterSpace↓ | On the trigger, opens the menu and focuses the first item. |
↑ | On the trigger, opens the menu and focuses the last item. |
↓↑ | Moves focus between items. Focus loops from the last item to the first. |
HomeEnd | Focuses the first or last item. |
→ | On a submenu trigger, opens the submenu and focuses its first item. |
← | In a submenu, closes it and returns focus to its trigger. |
EnterSpace | Activates the focused item. Checkbox and radio items toggle without closing. |
A–Z | Typeahead: focuses the next item whose label starts with the typed characters. |
Esc | Closes the menu and returns focus to the trigger. |
- Icon-only triggers need an
aria-labelthat names both the action and the object: Actions for Quarterly planning, not More. - Typeahead matches the item's text. When an item's content isn't plain text, set the
labelprop to what people would type. - Highlight vs. hover. Items highlight on pointer hover and keyboard focus alike (
data-highlighted), so there is only ever one active item.
API reference
DropdownMenu
The root. Accepts every prop of the Base UI Menu.Root.
DropdownMenuTrigger
The element that opens the menu. Use render to supply a Button.
DropdownMenuContent
The popup, rendered in a portal and positioned against the trigger. It also accepts every prop of the Base UI Menu.Popup. DropdownMenuContent renders its own portal, so you only need the exported DropdownMenuPortal when you compose a popup from the Base UI parts yourself.
DropdownMenuItem
DropdownMenuCheckboxItem
DropdownMenuRadioGroup
DropdownMenuRadioItem
DropdownMenuSub, DropdownMenuSubTrigger and DropdownMenuSubContent
DropdownMenuSub wraps a nested menu. DropdownMenuSubTrigger accepts inset, disabled, openOnHover (default true), delay (default 100) and closeDelay (default 0). DropdownMenuSubContent accepts the same positioning props as DropdownMenuContent, defaulting to side="right", align="start", alignOffset={-5} and sideOffset={2}.
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut
DropdownMenuLabel labels the surrounding DropdownMenuGroup and accepts inset. DropdownMenuSeparator draws a hairline between groups. DropdownMenuShortcut is a presentational <span> pushed to the item's trailing edge.