Dropdown menu
Radix-powered menu that opens from a trigger. The compound covers plain items, checkbox items, radio items, sub-menus, section labels, separators and keyboard shortcuts. Content chrome: rounded-md border bg-popover shadow-lg p-1 min-w-32 with a dialog-in entry animation. Items are focus:bg-muted with rounded-sm px-2 py-1.5 text-sm. The Dashboard uses this primitive under the hood for AccountMenu, row-level actions in Tables, and any custom action menu.
Anatomy
Fourteen exports for full menu semantics. Core is the Root → Trigger → Content triangle; the rest are the item primitives (Item / CheckboxItem / RadioItem), the grouping primitives (Group / Label / Separator / Sub), and the affordances (Shortcut for keyboard hints, RadioGroup for exclusive picks).
- 1DropdownMenu (Root)
Radix state container. defaultOpen / open / onOpenChange for controlled cases. modal={false} lets the user click outside without closing on touch.
- 2DropdownMenuTrigger
The button that opens the menu. Use asChild to wrap your own Button so keyboard focus + aria-expanded flow through. In the Dashboard it's the outline Button, the icon-only button pattern from the Button 'Icon-only with tooltip' recipe, or an Avatar for account menus.
- 3DropdownMenuContent
The floating panel. Portal + z-50 min-w-32 rounded-md border bg-popover shadow-lg p-1 with the dialog-in entry animation. Anchored to the trigger; controllable via align, side, sideOffset. Auto-focuses the first item on open.
- 4DropdownMenuItem
A row. rounded-sm px-2 py-1.5 text-sm gap-2 cursor-pointer with focus:bg-muted. Compose a Phosphor icon on the left (16px) + text + optional Shortcut. Use inset (or the Icon variant) when the row needs pl-8 to align its leading glyph with Checkbox / Radio indicator rows.
- 5DropdownMenuCheckboxItem
A toggleable row. Reserves pl-8 for the Check indicator on the left. checked prop controls state; onCheckedChange for the toggle.
- 6DropdownMenuRadioItem + DropdownMenuRadioGroup
Exclusive-select rows. RadioGroup owns the value; RadioItem carries the value. Indicator is a filled primary Circle (size-2) sitting in a pl-8 left slot.
- 7DropdownMenuLabel
Section caption. Level 1 (default) sits flush at px-2 for top-level section titles. Level 2 uses pl-8 so it aligns with the text of items that carry a leading indicator (Checkbox / Radio / Icon). Use to name a group above a few related items — never below.
- 8DropdownMenuSeparator
Hairline divider between logical groups. -mx-1 my-1 h-px bg-border so it visually spans the full width of the content padding.
- 9DropdownMenuShortcut
Right-aligned muted glyph for keyboard hints (⌘K, ⌘,). ml-auto text-xs tracking-widest text-muted-foreground. Do not add shortcuts users won't remember.
- 10DropdownMenuSub + SubTrigger + SubContent
Nested menu. Trigger shows a trailing CaretRight; Sub content mirrors Content chrome. Cap nesting at one level — deeper is unreachable on touch.
States
Content: entry via animate-dialog-in (fade + slide 8px + 0.97 scale). Item: idle transparent, focus:bg-muted (keyboard + hover), disabled: opacity-50 pointer-events-none. CheckboxItem / RadioItem show the indicator only when the row is checked / selected. Trigger tracks data-[state=open] for the icon-only button pattern (border + bg while the menu is open). SubTrigger picks up bg-accent when its Sub is open. Destructive item stays text-destructive in every state — the confirm dialog behind it does the guarding.
Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
The default Dashboard shape: an outline Button trigger + a menu of 3–6 items. Add a Separator + a destructive-intent action (Delete rule, Remove member) as the last item when the menu ends in a delete — pair it with a confirm Dialog instead of a red button.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>
Edit rule <DropdownMenuShortcut>⌘E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Rename</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Delete rule</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>The pattern the Dashboard uses in Table rows: the trigger reuses the 'Icon-only with tooltip' Button recipe (native button h-8 w-8 with hover border/bg + data-[state=open] styles) so the button surface reads active while the menu is open. Icon is DotsThreeOutline (fill). Wrap the trigger in a Tooltip to label it 'Actions'. Attach the menu with align='end' so it hugs the row's right edge.
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="Actions"
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition-all hover:border-border hover:bg-background hover:text-foreground hover:shadow-xs data-[state=open]:border-border data-[state=open]:bg-background data-[state=open]:text-foreground"
>
<DotsThreeOutline weight="fill" className="size-4" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>Actions</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>The workspace / account switcher pattern: a 48px-tall trigger with border rounded-md shadow-xs, an Avatar-32 on the left, a two-line text stack (name font-medium 14/20, description text-muted-foreground text-xs), and a trailing CaretUpDown 16px at 50% opacity. Wire it to a menu that leads with a User item (Avatar + name + email) followed by workspace / settings / logout sections split by Separators. Hover moves to bg-accent; focus keeps the ring; disabled goes opacity-50.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex h-12 w-56 items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-sm shadow-xs transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent"
>
<Avatar size="md">
<AvatarFallback>L</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate font-medium text-foreground">Leonardo</span>
<span className="truncate text-xs text-muted-foreground">leonardo@y.uno</span>
</div>
<CaretUpDown weight="light" className="size-4 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<div className="flex items-center gap-2 px-1 py-1.5">
<Avatar size="md">
<AvatarFallback>L</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-semibold text-foreground">Leonardo</span>
<span className="truncate text-xs text-muted-foreground">leonardo@y.uno</span>
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Workspace settings</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>For account menus and settings — group related items under a Label caption and separate groups with a Separator. Use Label level=2 (pl-8) when the group sits inside a menu that already reserves the indicator gutter so the caption aligns with the items' text. Never end the menu on a Label; every section must contain at least one interactive item.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Account</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Account</DropdownMenuLabel>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>
Settings <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Team</DropdownMenuLabel>
<DropdownMenuItem>Invite members</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Column visibility, notification categories, filter tags — anything the user picks multiple of. Use controlled state (checked + onCheckedChange) so the parent tracks the selection. Keep the menu open between clicks — that's Radix default.
function ColumnVisibility() {
const [status, setStatus] = React.useState(true);
const [country, setCountry] = React.useState(true);
const [amount, setAmount] = React.useState(true);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Funnel weight="light" className="size-4" />
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked={status} onCheckedChange={setStatus}>
Status
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={country} onCheckedChange={setCountry}>
Country
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={amount} onCheckedChange={setAmount}>
Amount
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
);
}For picking one option — sort order, view density, chart scale. Wrap the items in a RadioGroup so Radix enforces the exclusivity and manages the value.
function SortBy() {
const [value, setValue] = React.useState("date-desc");
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Sort by</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={value} onValueChange={setValue}>
<DropdownMenuRadioItem value="date-desc">Date · Newest first</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="date-asc">Date · Oldest first</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="amount">Amount</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="status">Status</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}For menus where every action has a semantic pair (User, Gear, ArrowUpRight, SignOut). Put a Phosphor icon 16px at the pl-8 slot so items align with any Checkbox / Radio row you may also render. Icons are Phosphor weight=light by default and follow the Yuno two-weight rule.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">More</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuItem className="pl-8">
<User weight="light" className="absolute left-2 size-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem className="pl-8">
<Gear weight="light" className="absolute left-2 size-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem className="pl-8">
<ArrowUpRight weight="light" className="absolute left-2 size-4" />
Open in new tab
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="pl-8">
<SignOut weight="light" className="absolute left-2 size-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Yuno reserves red for genuinely destructive actions (Delete account, Remove team). Use text-destructive on the item's text + icon; keep the row background transparent (no red fill, no red hover). The click MUST open a confirm Dialog — never mutate on the direct click.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>Rename</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onSelect={() => openConfirmDialog()}
>
Delete rule
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>One level of nesting for options that need their own micro-list (Move to → Folder A / Folder B, Share → Email / Copy link). SubTrigger shows the CaretRight; SubContent opens to the right by default. Cap at one level.
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Share</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuItem>Copy link</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Send via</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Slack</DropdownMenuItem>
<DropdownMenuItem>WhatsApp</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>Export as CSV</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Import
Full compound import from @/components/ui/dropdown-menu.
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuRadioGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from "@/components/ui/dropdown-menu";Props
Radix Dropdown menu props on each part. The most-used in prototypes:
| Prop | Type | Default | Description |
|---|---|---|---|
| DropdownMenu.open / defaultOpen | boolean | — | Controlled or uncontrolled open state. Controlled needs onOpenChange. |
| DropdownMenu.onOpenChange | (open: boolean) => void | — | Fires whenever the menu opens or closes. |
| DropdownMenu.modal | boolean | true | When true (Radix default), interaction outside the menu is blocked. Set false for menus that should coexist with the page (e.g. hover panels). |
| DropdownMenuTrigger.asChild | boolean | false | Merges props onto the child element (Radix Slot). Always pass when wrapping a real Button so keyboard focus + aria-expanded flow through. |
| DropdownMenuContent.side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side. Radix auto-flips if there's not enough room. |
| DropdownMenuContent.align | "start" | "center" | "end" | "center" | Alignment along the side. Row-action menus use "end" to hug the row's right edge; account menus use "start". |
| DropdownMenuContent.sideOffset | number | 4 | Distance in px between trigger and content. |
| DropdownMenuContent.className | string | — | Merged. Default is min-w-32. Set w-56 or wider for menus with checkbox/radio content so labels fit without truncation. |
| DropdownMenuItem.inset | boolean | false | Adds pl-8 so plain items align with checkbox/radio rows that reserve a left indicator slot. |
| DropdownMenuItem.onSelect | (event: Event) => void | — | Fires when the item is chosen (keyboard or click). Call event.preventDefault() to keep the menu open after selecting. |
| DropdownMenuItem.disabled | boolean | false | Disables the item — opacity-50 + pointer-events-none. |
| DropdownMenuCheckboxItem.checked / onCheckedChange | boolean / (checked: boolean) => void | — | Controlled state. Wrapping in useState is enough for prototypes. |
| DropdownMenuRadioGroup.value / onValueChange | string / (value: string) => void | — | Controlled group value. Each RadioItem carries its own value prop. |
| DropdownMenuLabel.inset | boolean | false | Adds pl-8 so the caption aligns with rows that carry an indicator (Figma 'Label / Level=2'). |
| DropdownMenuSub | component | — | Wrap a SubTrigger + SubContent to nest one level. Never nest deeper — the second sub-menu is unreachable on touch. |
When to use
- Row-level actions in a Table (Edit / Duplicate / Delete / Open) — the Dashboard's canonical Dots + DropdownMenu pattern.
- Account menus and settings entry points anchored to a Button or Avatar.
- Bulk actions on selected rows (Delete selected, Export, Tag).
- Preferences with multiple togglable options (column visibility, notification categories).
- Exclusive one-of-many pickers where a Select would feel too heavy (sort order, chart scale).
When not to use
- For a single action — just use a Button.
- For form fields where the user picks a value — use Select.
- For long or searchable lists — use Command.
- For arbitrary content (forms, tutorial popovers) — use Popover.
- For right-click / canvas menus — use Context menu (same compound but triggered by a right click).
Usage
- Group related items under a DropdownMenuLabel and split groups with a Separator.
- Keep menus short — 3–7 items reads well; longer lists belong in Command.
- Anchor row-action menus with align='end' so they hug the row's right edge.
- For row actions use the icon-only trigger + Tooltip 'Actions' — reuse Button's Icon-only recipe.
- Use shortcuts sparingly and only for actions the user repeats.
- Don't nest more than one sub-menu level — the second level is unreachable on touch.
- Don't mix Checkbox items and Radio items in the same visual group — they collide semantically.
- Don't end a menu on a Label — every section needs at least one interactive item.
- Don't paint a destructive item's background red — Yuno destructive items keep bg transparent, only the text + icon go text-destructive. The confirm Dialog behind it does the guarding.
- Don't use DropdownMenu for arbitrary content (forms, tutorial) — that's a Popover.
Related
Cross-links to atoms and patterns you may reach for next.
- Context menuSame compound API but right-click triggered. Use for canvas-level actions, not row actions.
- PopoverReach for Popover when the content is arbitrary (form snippet, tutorial). DropdownMenu is only for lists of actions.
- CommandUse Command when the list is long enough to search. Dropdown menus should stay short.
- ButtonThe icon-only trigger reuses Button's 'Icon-only with tooltip' recipe treatment.
- TooltipWrap icon-only triggers in a Tooltip so the button has a discoverable label.
- AvatarThe Avatar + text trigger uses Avatar size-8 with a text stack + CaretUpDown for the account / workspace switcher.
- Account menuThe organism that composes the Avatar + text trigger recipe with a User item, workspace picker and logout section.