Dropdown Menu
A trigger opens a list of actions the user runs once: rename, duplicate, share, delete, or go somewhere. The menu owns the surface, the positioning, and the keyboard path. The app owns what each item does and the result of it, shown inline via alert after the menu closes.
Actions only. No checkbox items, no radio items, and no submenu — a view-settings menu with toggles is Switch on the page, not a menu that stays open.
Parts
Eight parts: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, and DropdownMenuShortcut. DropdownMenuTrigger forces asChild and takes exactly one registry Button, the same rule as dialog. DropdownMenuContent takes only side and align — everything else about its positioning is fixed by the system. DropdownMenuGroup is semantic only, with no visual of its own; a group with a heading puts a DropdownMenuLabel as its first child. DropdownMenuShortcut is display only — the menu binds no key, the app owns the shortcut.
Item API
DropdownMenuItem takes variant (default or destructive), an icon, asChild, onSelect, and disabled. The icon slot exists only on an item that gets an icon, so an icon-free menu starts flush at the left edge. Give every item in a menu an icon or give none of them one — mixing the two leaves the text ragged. An action item uses onSelect; a navigation item wraps the router's Link with asChild. With asChild, an item with an icon renders the slot, then its children inside Radix Slot.Slottable, so the icon lands inside the Link alongside its own text — the link gets a real href, middle-click, and modifier-click, and choosing it with the keyboard follows it.
States
Radix merges hover and keyboard focus into one data-highlighted state: a background step to --accent, or to --error for a destructive item. The highlight snaps, by design — no transition, because a fade smears while arrowing quickly and native menus snap. An item draws no press ring and no focus ring: the highlight moves with focus and is the focus indicator. A disabled item still renders, at half opacity, so its position never shifts between opens. The panel itself takes outline-hidden for the pointer-opened case where Radix focuses the panel and not an item.
Panel
One size: a 32px item and a panel with p-1 and a min-w-32 floor. The panel caps its height to Radix's own available-height variable and scrolls internally, so a long list never runs off the viewport. loop stays false: ArrowDown on the last item stays put, matching macOS menus.
Icons and shortcuts
Every item in this menu carries an icon, so the labels line up past a shared slot, and Rename shows a DropdownMenuShortcut pushed to the trailing edge. Open the trigger to see them.
Live example
An overflow menu on a trip card: two labelled groups, a disabled item, action items on onSelect, and one navigation item as a Link through asChild — both item forms sit side by side. Choosing Delete trip opens the registry Dialog with no trigger of its own, dismissible={false}, and pending while the delete runs. The menu itself never holds open and shows no busyness — the dialog and, after it closes, the alert carry that.
Motion
Enter scales from 0.96 plus a fade over 250ms on the bounce curve, transform origin at the Radix popper variable so a flipped panel still grows from its trigger. Exit runs 150ms on the settle curve. The highlight itself carries no animation.
Accessibility
The trigger exposes aria-haspopup="menu" and aria-expanded. Enter, Space, or ArrowDown opens the menu with the first item highlighted; a pointer click opens it with no item highlighted. ArrowUp/ArrowDown move the highlight, Home/End jump, and typing runs typeahead. Tab is blocked inside the menu and disabled items are skipped. Escape and an outside click both close the menu and return focus to the trigger, including after a link item navigates within the same route tree. Every text pair meets WCAG AA in both modes.