Dropdown menu
A menu of actions or options that opens from a button.
INV-20931
Northwind Freight, $18,240.00, owned by Priya Raman
import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { Tooltip, TooltipContent, TooltipTrigger } from "@oration/canon/components/tooltip";import { CopyIcon, DownloadIcon, ExternalLinkIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon, UserIcon,} from "lucide-react";import Link from "next/link";import * as React from "react";export function Hero() { const [owner, setOwner] = React.useState("Priya Raman"); return ( <div className="flex w-full max-w-xl items-center gap-3 rounded-xl bg-card px-4 py-3 text-left shadow-border"> <div className="min-w-0 flex-1"> <p className="truncate text-13 font-medium">INV-20931</p> <p className="truncate text-xs text-muted-foreground"> Northwind Freight, $18,240.00, owned by {owner} </p> </div> <DropdownMenu> <Tooltip> <TooltipTrigger render={ <DropdownMenuTrigger render={ <Button type="button" variant="ghost" size="icon-sm" aria-label="More actions for INV-20931" /> } /> } > <MoreHorizontalIcon aria-hidden="true" /> </TooltipTrigger> <TooltipContent>More actions</TooltipContent> </Tooltip> <DropdownMenuContent align="end" className="w-56"> <DropdownMenuItem onClick={() => toast.add({ title: "Opened INV-20931" })} > <ExternalLinkIcon aria-hidden="true" /> Open invoice <DropdownMenuShortcut>↵</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuItem onClick={() => toast.add({ title: "Invoice duplicated", description: "INV-20932 is a draft for Northwind Freight.", }) } > <CopyIcon aria-hidden="true" /> Duplicate <DropdownMenuShortcut>⌘D</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuItem onClick={() => toast.add({ title: "Downloading INV-20931.pdf" }) } > <DownloadIcon aria-hidden="true" /> Download PDF </DropdownMenuItem> <DropdownMenuItem onClick={() => toast.add({ title: "Link copied" })} > <LinkIcon aria-hidden="true" /> Copy link </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuSub> <DropdownMenuSubTrigger> <UserIcon aria-hidden="true" /> Change owner </DropdownMenuSubTrigger> <DropdownMenuSubContent className="w-48"> <DropdownMenuRadioGroup value={owner} onValueChange={(value) => { setOwner(String(value)); toast.add({ title: `Owner changed to ${value}`, }); }} > {[ "Priya Raman", "Jordan Lee", "Aisha Bello", ].map((name) => ( <DropdownMenuRadioItem key={name} value={name} > {name} </DropdownMenuRadioItem> ))} </DropdownMenuRadioGroup> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuSeparator /> <DropdownMenuItem variant="destructive" onClick={() => toast.add({ type: "error", title: "INV-20931 voided", description: "It won't be paid in Friday's run.", }) } > <Trash2Icon aria-hidden="true" /> Void invoice </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> );}Usage#
Dropdown menu is a list of actions or options that opens from a button: a row's More actions, a toolbar's Sort, the user menu. Items can carry an icon and a shortcut, toggle as checkboxes, pick one value as a radio group, open a submenu, or remove something in Signal Red. It is the most common overlay in the product. The two things people get wrong are using it for navigation that belongs in the page, and hiding a view's main action inside it. Menus hold the secondary actions; the primary one stays a button.
When to use
- For a row's or record's secondary actions behind a More actions icon button: duplicate, download, copy link, delete.
- For view options that are a choice from a short list: Sort by, Group by, which columns show.
- For the user and workspace menus, with a label, groups and a theme submenu.
- For a split button's alternates, such as Save with Save as template.
When not to use
- To choose a value for a form field. A select shows the chosen value and belongs to the form. Use Select
- For controls such as switches, inputs or a segmented control. Use Popover
- For searching or running any command from the keyboard. Use Command menu
- For the same actions on right-click. Mirror this menu in a context menu instead of replacing it. Use Context menu
- For the one action the view exists for. Keep it a visible button. Use Button
Destructive is a tint
The Hairline-and-Lift Rule
Anatomy#
INV-20931
- Trigger. A button, often a ghost
icon-smwithMoreHorizontalIconand anaria-labelthat names the object. It keeps its hover fill while open. - Surface. Popover White, 10px corners, 4px padding, the overlay shadow and ring. At least 8rem wide, and as wide as the trigger unless you set a width.
- Group label. 12px medium Slate Meta, for naming a group or the object.
- Item. A 28px row with 8px corners, a 16px icon and 14px text, highlighted in Menu Hover.
- Shortcut. 12px Slate Meta at the end of the row.
- Submenu trigger. An item with a trailing chevron. It stays highlighted while its submenu is open.
- Separator. A Hairline that runs to the menu's edges, 4px above and below.
- Destructive item. Signal Red text and icon, last in the menu.
Examples#
Icons and shortcuts
Icons are 16px and sit before the label; DropdownMenuShortcut pushes the shortcut to the end. Items that navigate render a Link through render.
import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { ChevronDownIcon, DownloadIcon, FileTextIcon, SettingsIcon } from "lucide-react";import Link from "next/link";export function IconsAndShortcuts() { return ( <DropdownMenu> <DropdownMenuTrigger render={<Button type="button" variant="outline" size="sm" />} > <DownloadIcon data-icon="inline-start" aria-hidden="true" /> Export <ChevronDownIcon data-icon="inline-end" aria-hidden="true" /> </DropdownMenuTrigger> <DropdownMenuContent className="w-56"> <DropdownMenuItem onClick={() => toast.add({ title: "Exporting 212 invoices", description: "invoices-sep-2026.csv", }) } > <FileTextIcon aria-hidden="true" /> Export as CSV <DropdownMenuShortcut>⇧⌘E</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuItem onClick={() => toast.add({ title: "Exporting 212 invoices", description: "invoices-sep-2026.pdf", }) } > <DownloadIcon aria-hidden="true" /> Export as PDF </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem render={<Link href="/settings" />}> <SettingsIcon aria-hidden="true" /> Export settings </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> );}Checkbox items
For independent toggles such as visible columns. The menu stays open while you change several, and each checked item shows a check at the right.
import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { Columns3Icon } from "lucide-react";import * as React from "react";export function CheckboxItems() { const [columns, setColumns] = React.useState({ due: true, terms: true, owner: false, remittance: false, }); const labels = { due: "Due date", terms: "Terms", owner: "Owner", remittance: "Remittance", }; const shown = Object.values(columns).filter(Boolean).length; return ( <DropdownMenu> <DropdownMenuTrigger render={<Button type="button" variant="ghost" size="sm" />} > <Columns3Icon data-icon="inline-start" aria-hidden="true" /> Columns <span className="text-xs text-muted-foreground tabular-nums"> {shown + 2} </span> </DropdownMenuTrigger> <DropdownMenuContent className="w-48"> <DropdownMenuGroup> <DropdownMenuLabel>Show columns</DropdownMenuLabel> {(Object.keys(labels) as (keyof typeof labels)[]).map( (key) => ( <DropdownMenuCheckboxItem key={key} checked={columns[key]} onCheckedChange={(checked) => setColumns((current) => ({ ...current, [key]: checked, })) } > {labels[key]} </DropdownMenuCheckboxItem> ), )} </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> );}Radio group
For one choice from a short list, such as the sort order. The trigger says the current value. Pass closeOnClick when choosing should close the menu.
import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { ArrowDownUpIcon } from "lucide-react";import * as React from "react";export function RadioGroup() { const options = { due: "Due date", amount: "Amount", supplier: "Supplier name", received: "Date received", }; const [sort, setSort] = React.useState<keyof typeof options>("due"); return ( <DropdownMenu> <DropdownMenuTrigger render={<Button type="button" variant="ghost" size="sm" />} > <ArrowDownUpIcon data-icon="inline-start" aria-hidden="true" /> Sort: {options[sort]} </DropdownMenuTrigger> <DropdownMenuContent className="w-48"> <DropdownMenuGroup> <DropdownMenuLabel>Sort by</DropdownMenuLabel> <DropdownMenuRadioGroup value={sort} onValueChange={(value) => setSort(value as keyof typeof options) } > {(Object.keys(options) as (keyof typeof options)[]).map( (key) => ( <DropdownMenuRadioItem key={key} value={key} closeOnClick > {options[key]} </DropdownMenuRadioItem> ), )} </DropdownMenuRadioGroup> </DropdownMenuGroup> </DropdownMenuContent> </DropdownMenu> );}Labels, groups and separators
The account menu: a label naming who is signed in, groups of related items split by hairlines, and a labelled radio group for the theme.
import { Avatar, AvatarFallback } from "@oration/canon/components/avatar";import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { LogOutIcon, UserIcon, UserPlusIcon } from "lucide-react";import Link from "next/link";import * as React from "react";export function LabelsAndGroups() { const [theme, setTheme] = React.useState("system"); return ( <DropdownMenu> <DropdownMenuTrigger render={ <Button type="button" variant="ghost" className="h-10 gap-2 px-2" aria-label="Account menu for Maya Okafor" /> } > <Avatar size="sm"> <AvatarFallback>MO</AvatarFallback> </Avatar> <span className="text-13">Maya Okafor</span> </DropdownMenuTrigger> <DropdownMenuContent className="w-60"> <DropdownMenuGroup> <DropdownMenuLabel className="flex flex-col font-normal"> <span className="text-13 font-medium text-foreground"> Maya Okafor </span> <span>maya@cedarline.com</span> </DropdownMenuLabel> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuItem render={<Link href="/settings" />}> <UserIcon aria-hidden="true" /> Profile </DropdownMenuItem> <DropdownMenuItem onClick={() => toast.add({ title: "Invite link copied", description: "Anyone with the link can join Cedarline.", }) } > <UserPlusIcon aria-hidden="true" /> Invite teammates </DropdownMenuItem> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuGroup> <DropdownMenuLabel>Theme</DropdownMenuLabel> <DropdownMenuRadioGroup value={theme} onValueChange={(value) => setTheme(String(value))} > <DropdownMenuRadioItem value="light"> Light </DropdownMenuRadioItem> <DropdownMenuRadioItem value="dark"> Dark </DropdownMenuRadioItem> <DropdownMenuRadioItem value="system"> System </DropdownMenuRadioItem> </DropdownMenuRadioGroup> </DropdownMenuGroup> <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => toast.add({ title: "Signed out of Cedarline" }) } > <LogOutIcon aria-hidden="true" /> Sign out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> );}Destructive and disabled items
The destructive item is last, after a separator, in Signal Red with a 10% tint on highlight. Suspend Jordan Lee and Reset two-factor becomes disabled.
import { Button } from "@oration/canon/components/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,} from "@oration/canon/components/dropdown-menu";import { toast } from "@oration/canon/components/toast";import { MoreHorizontalIcon, Trash2Icon } from "lucide-react";import * as React from "react";export function DestructiveAndDisabled() { const [suspended, setSuspended] = React.useState(false); return ( <DropdownMenu> <DropdownMenuTrigger render={ <Button type="button" variant="ghost" size="icon-sm" aria-label="More actions for Jordan Lee" /> } > <MoreHorizontalIcon aria-hidden="true" /> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-56"> <DropdownMenuLabel>Jordan Lee</DropdownMenuLabel> <DropdownMenuItem disabled={suspended} onClick={() => toast.add({ title: "Two-factor reset" })} > Reset two-factor </DropdownMenuItem> <DropdownMenuItem onClick={() => { setSuspended((value) => !value); toast.add({ title: suspended ? "Jordan Lee reactivated" : "Jordan Lee suspended", }); }} > {suspended ? "Reactivate" : "Suspend"} </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem variant="destructive" onClick={() => toast.add({ type: "error", title: "Jordan Lee removed", description: "Their 14 open invoices moved to Maya Okafor.", }) } > <Trash2Icon aria-hidden="true" /> Remove from Cedarline </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> );}States#
import { cn } from "@oration/canon/lib/utils";import { CheckIcon, ChevronRightIcon } from "lucide-react";export function StatesMatrix() { const item = "relative flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm [&_svg]:size-4 [&_svg]:shrink-0"; const rows = [ { state: "Rest", className: "", label: "Duplicate", check: false }, { state: "Highlighted", className: "bg-accent", label: "Duplicate", check: false, }, { state: "Checked", className: "pr-8", label: "Due date", check: true }, { state: "Submenu open", className: "bg-accent", label: "Change owner", check: false, }, { state: "Disabled", className: "opacity-50", label: "Reset two-factor", check: false, }, { state: "Destructive", className: "text-destructive", label: "Void invoice", check: false, }, { state: "Destructive highlighted", className: "bg-destructive/10 text-destructive dark:bg-destructive/20", label: "Void invoice", check: false, }, ]; return ( <div className="grid w-full max-w-md grid-cols-[10rem_minmax(0,1fr)] items-center gap-x-4 gap-y-1" inert > {rows.map((row) => ( <div key={row.state} className="contents"> <span className="text-13 text-muted-foreground"> {row.state} </span> <div className={cn(item, row.className)}> {row.label} {row.state === "Submenu open" ? ( <ChevronRightIcon aria-hidden="true" className="ml-auto" /> ) : null} {row.check ? ( <span className="absolute right-2 flex items-center"> <CheckIcon aria-hidden="true" /> </span> ) : null} </div> </div> ))} </div> );}| State | Treatment |
|---|---|
| Closed | The trigger has aria-expanded="false". |
| Opening | Grows from a 0.97 scale and slides 8px from the trigger over 160ms on the house ease-out. |
| Item rest | No fill. Text in Popover ink, icons 16px. |
| Item highlighted | Menu Hover fill from pointer or arrow keys; the highlight follows the pointer (highlightItemOnHover). |
| Destructive highlighted | A 10% Signal Red tint (20% in dark) behind red text. |
| Checked | Checkbox and radio items show a 16px check at the right edge. |
| Submenu open | The submenu trigger keeps its Menu Hover fill. |
| Disabled | 50% opacity and no pointer events. Arrow keys still reach it, so screen reader users hear that it exists, but it can't be chosen. |
| Closing | Fades out toward a 0.95 scale in 110ms. |
Behavior#
- Built on Base UI Menu. Click, Enter, Space or Down on the trigger opens it. Focus moves into the menu, arrow keys move the highlight and wrap at the ends, and typing jumps to the item that starts with those letters.
- Choosing an item runs its
onClickand closes the menu. Checkbox and radio items stay open by default so several can be changed; setcloseOnClickto change either. - Escape or a click outside closes it, and focus returns to the trigger.
- Modal by default: while open, the page doesn't scroll and outside clicks only close the menu. Pass
modal={false}for menus in toolbars that shouldn't lock the page. aligndefaults to start, so the menu lines up with the trigger's left edge; row action menus at the right of a row usealign="end". It flips and shifts to stay on screen, and scrolls inside the available height.- Submenus open to the right on hover after 100ms, or with Right or Enter, and close with Left or Escape. They reposition to the left near the viewport edge.
- Menu items that navigate render a Next
Linkthroughrender, so they keep link behavior. - Motion is shared with popovers and selects: 160ms in from 0.97 and 110ms out, a fade only under reduced motion.
Do and don't#
Content#
- Items start with a verb and name the object when it isn't obvious: Download PDF, Copy link, Remove from Cedarline.
- Use the same verb the rest of the product uses: if the button says Void invoice, the menu does too.
- Option items are nouns in sentence case: Due date, Amount, Supplier name.
- An item that opens a dialog ends with an ellipsis only if the dialog asks for more input: Move to payment run….
- Shortcuts use symbols: ⌘D, ⇧⌘C.
Accessibility#
- The trigger gets
aria-haspopup="menu"andaria-expanded; the surface isrole="menu", items aremenuitem,menuitemcheckboxormenuitemradiowitharia-checked. - Icon-only triggers need an
aria-labelthat names the object, such as More actions for INV-20931, and a tooltip. - Group labels name their group for screen readers through
Menu.GroupLabel; wrap labelled items in aDropdownMenuGroup. - Item icons are decorative; the label names the action.
- Focus returns to the trigger on close, so keyboard users land where they started.
- Rows are 28px, above the 24px minimum target.
| Keys | Action |
|---|---|
| Enter | Opens the menu, or runs the highlighted item. |
| Space | Opens the menu, or runs the highlighted item. |
| ↓↑ | Moves the highlight, wrapping at the ends. Down on the trigger opens the menu. |
| Home | Highlights the first item. |
| End | Highlights the last item. |
| → | Opens the highlighted submenu. |
| ← | Closes the submenu and returns to its trigger. |
| Esc | Closes the menu and returns focus to the trigger. |
| A–Z | Jumps to the next item starting with that letter. |
Design tokens#
| Token | Used for |
|---|---|
--popover | Surface fill |
--accent | Highlighted item, open submenu trigger |
--destructive | Destructive text, icon and its 10% and 20% tints |
--muted-foreground | Group labels and shortcuts |
--border | Separators |
shadow-md | The overlay shadow; submenus use shadow-lg |
--radius-lg | 10px surface corners |
--radius-md | 8px item corners |
API reference#
DropdownMenu
The root. Holds the open state.
Other props spread onto Base UI Menu.Root.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | No default | Controls whether it is open. |
defaultOpen | boolean | false | Whether it starts open when uncontrolled. |
onOpenChange | (open: boolean, eventDetails) => void | No default | Called when it opens or closes. |
modal | boolean | true | Locks page scroll and outside interaction while open. |
loopFocus | boolean | true | Wraps the highlight at the ends. |
highlightItemOnHover | boolean | true | Moves the highlight with the pointer. |
disabled | boolean | false | Stops the menu from opening. |
DropdownMenuTrigger
The button that opens the menu.
Other props spread onto Base UI Menu.Trigger (<button>).
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | (props, state) => ReactElement | No default | Render your own button, such as <Button variant="ghost" size="icon-sm" />. |
openOnHover | boolean | false | Also opens on hover, for menubars. |
delay | number | 100 | Hover delay with openOnHover. |
disabled | boolean | false | Disables the trigger. |
DropdownMenuContent
The surface, with its portal and positioner.
Other props spread onto Base UI Menu.Popup.
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Alignment along the trigger. Row menus use end. |
side | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | "bottom" | Which side of the trigger it opens on. |
sideOffset | number | 4 | Distance from the trigger in pixels. |
alignOffset | number | 0 | Offset along the alignment axis. |
className | string | No default | Merged after the defaults. Set a width such as w-56; otherwise the menu matches the trigger's width, at least 8rem. |
DropdownMenuItem
An action.
Other props spread onto Base UI Menu.Item (<div>).
| Prop | Type | Default | Description |
|---|---|---|---|
onClick | (event) => void | No default | Runs the action. The menu then closes. |
variant | "default" | "destructive" | "default" | destructive draws the item in Signal Red. |
inset | boolean | false | Indents the label 28px to line up with items that have icons or checks. |
disabled | boolean | false | Dims the item and stops it running. It stays reachable by arrow keys. |
closeOnClick | boolean | true | Closes the menu after the click. |
render | ReactElement | (props, state) => ReactElement | No default | Render a Next Link for items that navigate. |
DropdownMenuCheckboxItem
An item that toggles on and off, with a check at the right.
Other props spread onto Base UI Menu.CheckboxItem.
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | No default | Whether it is checked, when controlled. |
defaultChecked | boolean | false | Initial state when uncontrolled. |
onCheckedChange | (checked: boolean, eventDetails) => void | No default | Called when it toggles. |
closeOnClick | boolean | false | Keeps the menu open so several can be toggled. |
inset | boolean | false | Indents the label 28px. |
DropdownMenuRadioGroup
Groups radio items that share one value.
Other props spread onto Base UI Menu.RadioGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
value | any | No default | The selected value, when controlled. |
defaultValue | any | No default | Initial value when uncontrolled. |
onValueChange | (value: any, eventDetails) => void | No default | Called when a radio item is chosen. |
DropdownMenuRadioItem
One option in a radio group, with a check at the right when selected.
Other props spread onto Base UI Menu.RadioItem.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | any | No default | The value it selects. |
closeOnClick | boolean | false | Closes the menu after choosing. |
inset | boolean | false | Indents the label 28px. |
DropdownMenuSub
The root of a submenu.
Other props spread onto Base UI Menu.SubmenuRoot.
No props of its own.
DropdownMenuSubTrigger
The item that opens a submenu, with a trailing chevron.
Other props spread onto Base UI Menu.SubmenuTrigger.
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Indents the label 28px. |
disabled | boolean | false | Disables the submenu. |
delay | number | 100 | Hover delay before it opens. |
DropdownMenuSubContent
The submenu surface. Opens to the right, aligned to the trigger's top.
Other props spread onto DropdownMenuContent.
| Prop | Type | Default | Description |
|---|---|---|---|
side | string | "right" | Which side it opens on. |
alignOffset | number | -3 | Lines the first item up with the trigger. |
className | string | No default | Set a width such as w-48 or w-64. |
DropdownMenuLabel
A 12px Slate Meta label for a group.
Other props spread onto Base UI Menu.GroupLabel.
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Indents it 28px. |
DropdownMenuGroup
Groups items under a label.
Other props spread onto Base UI Menu.Group.
No props of its own.
DropdownMenuSeparator
A Hairline between groups.
Other props spread onto Base UI Menu.Separator.
No props of its own.
DropdownMenuShortcut
A shortcut at the end of an item.
Other props spread onto <span>.
No props of its own.
DropdownMenuPortal
The portal, exported for custom compositions. DropdownMenuContent already includes it.
Other props spread onto Base UI Menu.Portal.
No props of its own.
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
DropdownMenuContent defaults to the trigger's width (w-(--anchor-width), at least 8rem), so a menu opened from an icon button is 8rem wide and truncates. Nearly every call site passes a width such as w-56.
DropdownMenuShortcut sets tracking-widest, letter-spacing the shortcut against the no letter-spaced captions rule, and writes plain text where tooltips use Kbd chips. Only the app sidebar's help menu uses it.
Base UI's Menu.LinkItem isn't wrapped. Navigating items render a Link through DropdownMenuItem's render, which works but keeps the menuitem role on a link.
Submenus use shadow-lg, the dialog shadow, where DESIGN.md gives every menu the overlay shadow.
Menus enter from 0.97 but exit with zoom-out-95 to 0.95.