Dropdown Menu
A contextual menu that opens from a trigger, built on Base UI. Supports grouped items, leading/trailing icons, keyboard shortcuts, descriptions, submenus, checkboxes, radio groups, and full RTL layout.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@tessinaui/ui";
import { Button } from "@tessinaui/ui";<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Open</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem intent="error">Sign out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>Examples
Default
A basic menu with a label, items, and a destructive sign-out action.
Sizes
Five sizes from xs to xl, each scaling padding, text, and icons. Open each menu to compare the scale.
With Icons & Shortcuts
Items with leading icons and keyboard shortcut badges.
Checkbox Items
Toggleable checkbox items that hold their own checked state.
Radio Group
Mutually exclusive options grouped in a radio group.
Column toggles
The column-visibility menu: each row is a DropdownMenuSwitchItem — semantically a menu checkbox painted as a switch, so the menu stays open while toggling. A search field cannot live inside a role="menu" (searchbox is not an allowed menu descendant) — for a searchable list of options, reach for the Command component instead.
Header & Footer
An identity block pinned above the actions and a hint bar pinned below them — both hold their edges while a long list of items scrolls between them.
Submenu
Nested submenus for grouping related actions behind a trigger.
API Reference
DropdownMenu
The root. It owns open state and nothing visual — all Base UI Menu.Root props are forwarded.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Uncontrolled initial state |
onOpenChange | (open: boolean, event, reason) => void | — | Fires on every open and close; reason names the dismissal ("trigger-press", "outside-press", "escape-key"…) so you can ignore specific ones |
modal | boolean | true | Trap focus and block the page behind the menu while it is open |
DropdownMenuTrigger
The control that opens the menu. Pass asChild to merge the trigger's props onto your own element instead of rendering a button — it's a compat alias for Base UI's render, and both work.
DropdownMenuPortal
Escape hatch for placing the popup yourself. DropdownMenuContent already portals to document.body by default, so reach for this only when you need a different container and container doesn't fit.
DropdownMenuContent
The panel that contains menu items. Pass size and width here — they propagate automatically to all child items via React context.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Controls panel padding, item padding, text size, and icon size |
width | "narrow" | "default" | "wide" | "default" | Controls minimum width of the panel |
side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side to open on |
align | "start" | "center" | "end" | — | Alignment relative to the trigger. Unset, Base UI's own default (center) applies |
sideOffset | number | 4 | Gap in px between trigger and panel |
alignOffset | number | 0 | Shift in px along the side axis |
collisionPadding | number | 8 | Minimum gap from the viewport edges before the menu flips or shifts |
rounded | "none" | "sm" | "md" | "lg" | "full" | — | Popup corner radius, propagated to every item proportionally. Left unset, the popup and its items keep radii derived from size — a deliberate exception that preserves menus written before this prop existed |
dir | "ltr" | "rtl" | — | Text direction — flips icon order, logical padding, and submenu side |
positionerClassName | string | — | Extra classes on the positioner — raise its z-50 when the menu sits in a higher stacking context |
arrow | boolean | false | Render a speech-bubble tail pointing at the trigger |
portal | boolean | true | Render inside a portal at document.body |
container | HTMLElement | null | document.body | Portal target |
inert | boolean | false | Render the popup as a static, non-interactive illustration — nothing inside can take focus |
collisionAvoidance | object | — | Base UI collision config — pass { side: "none" } to keep a declared side instead of flipping at a viewport edge |
All other Base UI Menu.Popup props are forwarded. The popup clamps itself to the viewport: width never exceeds 100dvw - 2rem, and a menu taller than the available space scrolls instead of clipping.
DropdownMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
leadingIcon | React.ReactNode | — | Icon on the start side |
trailingIcon | React.ReactNode | — | Icon on the end side |
shortcut | string | string[] | — | Keyboard shortcut hint. Prefer the array of canonical names — ["mod", "K"] renders ⌘K on Apple keyboards and Ctrl+K elsewhere. A literal string ("⌘K") still works |
badge | React.ReactNode | — | Slot beside the label for a status pill — pass a <Badge> ("New", "Beta", a count) |
description | string | — | Secondary description line below the label |
intent | "none" | "error" | "none" | "error" applies destructive (red) styling to text and icons |
inset | boolean | false | Adds start padding for alignment when some items have icons and others don't |
disabled | boolean | false | Disables the item and mutes its appearance |
closeOnClick | boolean | true | Pass false to keep the menu open after the item is activated. Caveat: the press-on-trigger → drag → release-over-item gesture still closes a plain item |
All native Base UI Menu.Item props are forwarded.
Portalling & collision
DropdownMenuContent and DropdownMenuSubContent also take portal (default true; false renders the popup — focus guards included — inside the component's own subtree, where it scales with transformed containers and can be covered by one inert ancestor), container (portal target), collisionAvoidance (Base UI collision config — pass { side: "none" } to keep a declared side instead of flipping away from a viewport edge), and inert (render the popup as a non-interactive illustration).
portal has a Portal toggle in the playground above — turn it off and the menu scales with the preview's zoom instead of being laid out against document.body at 100%, which is the whole reason the escape hatch exists.
container, collisionAvoidance and inert are deliberately reference-only: all three exist for embedding a menu in a harness you control — a static screenshot, a canvas that owns its own stacking context — and any example would be a picture of scaffolding rather than of the menu. Use the signatures above.
DropdownMenuLinkItem
A menu row that renders a real <a href>. Use it for every navigation item — "Open in new tab", "View on GitHub" — instead of an onClick that calls window.open: a real anchor keeps middle-click, ⌘-click, the context menu, and the status-bar URL preview. Takes the same row props as DropdownMenuItem (leadingIcon, trailingIcon, shortcut, badge, description, intent, inset) plus all <a> props (href, target, rel…).
<DropdownMenuLinkItem href="/settings" leadingIcon={<Settings />}>
Settings
</DropdownMenuLinkItem>
<DropdownMenuLinkItem href="https://github.com/acme" target="_blank" rel="noreferrer" trailingIcon={<ExternalLink />}>
View on GitHub
</DropdownMenuLinkItem>closeOnClick defaults to false here (Base UI): a plain click navigates away — the page unload is the close — while a ⌘-click opens a background tab and correctly leaves the menu open. SPA route changes that do not unload the page should pass closeOnClick explicitly.
DropdownMenuSwitchItem
A toggle row painted as a switch — the column-visibility / settings-menu idiom. Semantically a Menu.CheckboxItem (role="menuitemcheckbox", announced "checked/unchecked"), so the menu stays open on toggle and keyboard behaviour is the menu's own.
| Prop | Type | Default | Description |
|---|---|---|---|
checked / onCheckedChange | boolean / (checked, eventDetails) => void | — | Controlled toggle state |
leadingIcon | React.ReactNode | — | Icon on the start side |
description | string | — | Secondary line below the label |
disabled | boolean | false | Disables the row |
The switch is presentation only — never nest a real focusable control (a <Switch>, a <button>) inside any menu item: role="menuitem*" must not contain focusable descendants, and the menu's roving tabindex would fight the inner control.
Rounded
Explicit rounded values, popup and item. Left unset, both keep radii
derived from size instead (a deliberate §2 exception — see the prop table
above); set it and the cascade governs everything.
| Value | Popup | Item |
|---|---|---|
"none" | rounded-none | rounded-none |
"sm" | rounded-md | rounded-sm |
"md" | rounded-lg | rounded-md |
"lg" | rounded-xl | rounded-lg |
"full" | rounded-3xl | rounded-xl |
The popup column is the same scale Popover
uses — one radius vocabulary across the popup family. full was rounded-2xl
(16px) until 0.3.0, which made it a reduction at md, lg and xl, whose
size-derived radii are already 24, 32 and 36px.
DropdownMenuArrow
Optional macOS-style tail pointing at the trigger. Opt in with arrow on DropdownMenuContent — there is an Arrow toggle in the playground above — or place <DropdownMenuArrow/> explicitly when you want to style it. The tail's dome follows rounded and its position clears the popup's corner radius automatically.
DropdownMenuArrow, DropdownMenuShortcut and DropdownMenuPortal are exported but have no example of their own, and that is deliberate: each is rendered for you by the prop that means it — arrow on the content, shortcut on an item, and the portal the content sets up itself. The bare exports exist for the case where you are assembling a row or a popup by hand, so an example of one would be an example of the plumbing, not of a menu.
<DropdownMenuContent arrow>…</DropdownMenuContent>DropdownMenuBackdrop
Scrim rendered under the open menu — same treatment as PopoverBackdrop. Place it as a sibling of DropdownMenuContent for mobile/modal menus that want the page dimmed while choosing.
DropdownMenuHeader / DropdownMenuFooter
Non-interactive bands pinned to the top and bottom of the menu — an account block above the actions, a hint bar or "Create new…" row below them. They stay put while the item list scrolls.
| Prop | Type | Default | Description |
|---|---|---|---|
sticky | boolean | true | Pin to the edge while items scroll |
separator | boolean | true | Hairline between the band and the items |
DropdownMenuLabel
Section heading inside the menu. Automatically sized to match the content size. Place it inside the DropdownMenuGroup it names — the label is then announced as that group's accessible name; standalone it renders its own group.
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | false | Adds start padding to align with inset items |
DropdownMenuSeparator
Horizontal divider between groups. No props needed.
DropdownMenuShortcut
Standalone keyboard hint, built on the design system's Shortcut/Key primitives. Used internally by DropdownMenuItem but can be composed manually.
{/* Preferred: canonical names, platform-resolved — ⌘K on Apple, Ctrl+K elsewhere */}
<DropdownMenuShortcut>{["mod", "K"]}</DropdownMenuShortcut>
{/* Literal glyph run still works (one keycap per key) */}
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>| Prop | Type | Default | Description |
|---|---|---|---|
children | string | string[] | — | Array of canonical key names (preferred) or a literal string |
glyphs | boolean | array form only | Render Apple symbols on Apple platforms |
platform | "mac" | "other" | detected | Force a keyboard convention (previews, tests) |
DropdownMenuSub / SubTrigger / SubContent
Three-part pattern for nested submenus:
<DropdownMenuSub>
<DropdownMenuSubTrigger leadingIcon={<Share />}>Share</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Copy link</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>DropdownMenuSubTrigger accepts leadingIcon and inset. DropdownMenuSubContent inherits size and rounded from parent context and accepts its own width and collisionPadding.
DropdownMenuCheckboxItem
Checkbox-style item that maintains checked state:
const [checked, setChecked] = useState(false);
<DropdownMenuCheckboxItem checked={checked} onCheckedChange={setChecked}>
Show toolbar
</DropdownMenuCheckboxItem>DropdownMenuRadioGroup / RadioItem
Radio-group for mutually exclusive selection:
const [theme, setTheme] = useState("system");
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
<DropdownMenuRadioItem value="light">Light</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">System</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>DropdownMenuSkeleton
A loading placeholder for the panel. It takes its geometry from the menu's own class sources — the same contentSizeVariants and itemSizeVariants the real popup and rows use — so the swap from placeholder to content doesn't shift the layout.
| Prop | Type | Default | Description |
|---|---|---|---|
size, width, rounded | — | "md", "default", — | Match these to the menu you're standing in for |
items | number | 6 | Row placeholders to render |
showLabel | boolean | false | Render a label bar above the rows |
showIcons | boolean | true | Leading-icon placeholder on each row |
showShortcuts | boolean | false | Trailing shortcut placeholder on each row |
Sizes
All five sizes scale item padding, text size, icon size, and panel padding proportionally. Pass size once to DropdownMenuContent:
<DropdownMenuContent size="sm">
<DropdownMenuItem>Profile</DropdownMenuItem>
</DropdownMenuContent>Width Variants
{/* Compact narrow panel */}
<DropdownMenuContent width="narrow">...</DropdownMenuContent>
{/* Default width (min-w-[12rem]) */}
<DropdownMenuContent width="default">...</DropdownMenuContent>
{/* Wide panel — useful for items with descriptions */}
<DropdownMenuContent width="wide">...</DropdownMenuContent>With Icons, Shortcut & Description
<DropdownMenuItem
leadingIcon={<Settings />}
shortcut={["mod", ","]}
description="Manage your preferences"
>
Settings
</DropdownMenuItem>Destructive Items
<DropdownMenuItem intent="error" leadingIcon={<Trash />} shortcut={["mod", "backspace"]}>
Delete
</DropdownMenuItem>RTL
Pass dir="rtl" to DropdownMenuContent. The layout, icon positions, and submenu open direction all flip automatically:
<DropdownMenuContent dir="rtl">
<DropdownMenuItem leadingIcon={<User />}>الملف الشخصي</DropdownMenuItem>
</DropdownMenuContent>Accessibility
- Built on Base UI
@base-ui/react/menu— full keyboard navigation out of the box - Arrow keys move focus between items;
Enter/Spaceactivates;Escapecloses Tabdismisses the menu and returns focus to the trigger- Each item is a focusable
[role="menuitem"] - Disabled items have
[data-disabled]andpointer-events-none - Color contrast: all intent states meet WCAG 2.1 AA
- Submenus open on
ArrowRight(LTR) /ArrowLeft(RTL)