DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
PopoverHoverCardDropdown MenuContextMenuModalAlertDialogDrawerAction Sheet
AI agentsContributing
  1. Components
  2. Dropdown Menu

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.

$ pnpm add @tessinaui/ui

When to use

  • A short list of commands behind a trigger: Rename, Duplicate, Delete. Choosing a row does something at once.
  • Row and card actions that would crowd the layout as visible buttons. An icon trigger needs an aria-label.
  • Grouped actions. Use DropdownMenuLabel and DropdownMenuSeparator to group rows, and put the destructive one (intent="error") last.
  • View settings that toggle on the spot: DropdownMenuCheckboxItem, a radio group, or a switch row that keeps the menu open.
  • Related actions behind one row, with a submenu. Keep nesting shallow.

When not to use

  • To pick a value a form submits. Use Select; a menu's radio group is for view settings.
  • When one action is the default and the rest are alternatives. Use SplitButton.
  • For right-click actions on content. Use Context Menu.
  • For a large set of commands the user must search. Use Command.
  • For a few actions that fit on screen. Show them as buttons; hiding them costs a click.
  • For rows that belong on the page, such as a settings list. Use Menu Item.
  • For rich or interactive content like forms and fields. Use Popover.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
} from "@tessinaui/ui";
import { Button } from "@tessinaui/ui";
example.tsx
<DropdownMenu>
  <DropdownMenuTrigger render={<Button variant="outline">Open</Button>} />
  <DropdownMenuContent>
    <DropdownMenuLabel>My Account</DropdownMenuLabel>
    <DropdownMenuSeparator />
    <DropdownMenuItem>Profile</DropdownMenuItem>
    <DropdownMenuItem>Settings</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem intent="error">Sign out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

For the same rows on the page rather than in a popup — a settings list, an account panel — use Menu Item.

Reach for a dropdown menu when the trigger offers actions — rows that do something the moment they are chosen. To pick a value that a form submits, use Select: a menu's radio group is for view settings, not form fields. A list long enough to need search belongs in Command, actions on whatever sits under the pointer in Context Menu, and an app's File / Edit / View bar in Menubar.

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.

On-color

On a coloured Surface the trigger takes the plate's ink and, while its menu is open, the plate's inverse chip — tone resolves from context, no prop needed. The menu stays a standard panel: a portal floats above the plate, it is not on it, so its rows, shortcuts and destructive item read exactly as they do on the page.

RTL

dir="rtl" on the root mirrors every popup and its keyboard: the submenu opens toward the start edge, ArrowLeft opens it and ArrowRight closes it.

API Reference

DropdownMenu

The root. It owns open state and nothing visual — all Base UI Menu.Root props are forwarded.

PropTypeDefaultDescription
openboolean—Controlled open state
defaultOpenbooleanfalseUncontrolled 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
modalbooleantrueTrap focus and block the page behind the menu while it is open
dir"ltr" | "rtl"—Reading direction for the whole menu — every popup, the submenu side and the arrow keys that open and close submenus. Unset, the menu inherits the page

DropdownMenuTrigger

The control that opens the menu. It renders a <button>; to open the menu from your own element, pass that element as render — render={<Button variant="outline" />}. The trigger's props and ref merge onto it.

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.

PropTypeDefaultDescription
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
sideOffsetnumber4Gap in px between trigger and panel
alignOffsetnumber0Shift in px along the side axis
collisionPaddingnumber8Minimum 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"—Reading direction for this popup — flips icon order, logical padding, submenu side and submenu arrow keys. Overrides the root's dir; unset, inherits it
positionerClassNamestring—Extra classes on the positioner — raise its z-50 when the menu sits in a higher stacking context
arrowbooleanfalseRender a speech-bubble tail pointing at the trigger
portalbooleantrueRender inside a portal at document.body
containerHTMLElement | nulldocument.bodyPortal target
inertbooleanfalseRender the popup as a static, non-interactive illustration — nothing inside can take focus
collisionAvoidanceobject—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

PropTypeDefaultDescription
leadingIconReact.ReactNode—Icon on the start side
trailingIconReact.ReactNode—Icon on the end side
shortcutstring | string[]—Keyboard shortcut hint. Prefer the array of canonical names — ["mod", "K"] renders ⌘K on macOS and Ctrl+K elsewhere. A literal string ("⌘K") still works
badgeReact.ReactNode—Slot beside the label for a status pill — pass a <Badge> ("New", "Beta", a count)
descriptionstring—Secondary description line below the label
intent"none" | "error""none""error" applies destructive (red) styling to text and icons
insetbooleanfalseAdds start padding for alignment when some items have icons and others don't
disabledbooleanfalseDisables the item and mutes its appearance
closeOnClickbooleantruePass 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 source" — 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…).

example.tsx
<DropdownMenuLinkItem href="/settings" leadingIcon={<Settings />}>
  Settings
</DropdownMenuLinkItem>

<DropdownMenuLinkItem href="https://github.com/acme" target="_blank" rel="noreferrer" trailingIcon={<ExternalLink />}>
  View source
</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.

PropTypeDefaultDescription
checked / onCheckedChangeboolean / (checked, eventDetails) => void—Controlled toggle state
leadingIconReact.ReactNode—Icon on the start side
descriptionstring—Secondary line below the label
disabledbooleanfalseDisables 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.

ValuePopupItem
"none"rounded-nonerounded-none
"sm"rounded-mdrounded-sm
"md"rounded-lgrounded-md
"lg"rounded-xlrounded-lg
"full"rounded-3xlrounded-2xl

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.

example.tsx
<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.

PropTypeDefaultDescription
stickybooleantruePin to the edge while items scroll
separatorbooleantrueHairline 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.

PropTypeDefaultDescription
insetbooleanfalseAdds 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.

example.tsx
{/* Preferred: canonical names, platform-resolved — ⌘K on macOS, Ctrl+K elsewhere */}
<DropdownMenuShortcut>{["mod", "K"]}</DropdownMenuShortcut>

{/* Literal glyph run still works (one keycap per key) */}
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
PropTypeDefaultDescription
childrenstring | string[]—Array of canonical key names (preferred) or a literal string
glyphsbooleanarray form onlyRender the ⌘ ⇧ ⌥ ⌃ symbols on macOS and iOS
platform"mac" | "other"detectedForce a keyboard convention (previews, tests)

DropdownMenuSub / SubTrigger / SubContent

Three-part pattern for nested submenus:

example.tsx
<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:

example.tsx
const [checked, setChecked] = useState(false);

<DropdownMenuCheckboxItem checked={checked} onCheckedChange={setChecked}>
  Show toolbar
</DropdownMenuCheckboxItem>

DropdownMenuRadioGroup / RadioItem

Radio-group for mutually exclusive selection:

example.tsx
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.

PropTypeDefaultDescription
size, width, rounded—"md", "default", —Match these to the menu you're standing in for
itemsnumber6Row placeholders to render
showLabelbooleanfalseRender a label bar above the rows
showIconsbooleantrueLeading-icon placeholder on each row
showShortcutsbooleanfalseTrailing 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:

example.tsx
<DropdownMenuContent size="sm">
  <DropdownMenuItem>Profile</DropdownMenuItem>
</DropdownMenuContent>

Width Variants

example.tsx
{/* 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

example.tsx
<DropdownMenuItem
  leadingIcon={<Settings />}
  shortcut={["mod", ","]}
  description="Manage your preferences"
>
  Settings
</DropdownMenuItem>

Destructive Items

example.tsx
<DropdownMenuItem intent="error" leadingIcon={<Trash />} shortcut={["mod", "backspace"]}>
  Delete
</DropdownMenuItem>

RTL

Pass dir="rtl" to DropdownMenu. It reaches every popup: the layout, icon positions, submenu side, and the arrow keys that open and close submenus all flip. A dir on DropdownMenuContent works too, and overrides the root's for that popup:

example.tsx
<DropdownMenu dir="rtl">
  <DropdownMenuTrigger>…</DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuItem leadingIcon={<User />}>الملف الشخصي</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

Left unset, nothing is stamped and the popup follows the page. Base UI reads keyboard direction from this prop only — never from the page — so an RTL page should still pass it.

Accessibility

VoiceOver + Safari: passed 2026-09-14. The trigger reads "Open menu, menu pop up button". Space opens on "Profile, menu item (1 of 3)"; Down reads "Settings, menu item (2 of 3)" and "Sign out, destructive action, menu item (3 of 3)"; Escape returns to the trigger. NVDA and JAWS are untested.

  • Built on Base UI @base-ui/react/menu — full keyboard navigation out of the box
  • Arrow keys move focus between items; Enter/Space activates; Escape closes
  • Tab dismisses the menu and returns focus to the trigger
  • Each item is a focusable [role="menuitem"]
  • Disabled items have [data-disabled] and pointer-events-none
  • Color contrast: all intent states meet WCAG 2.1 AA
  • Submenus open on ArrowRight (LTR) / ArrowLeft (RTL)
HoverCardContextMenu

On this page

When to useWhen not to useInstallationUsageExamplesDefaultSizesWith Icons & ShortcutsCheckbox ItemsRadio GroupColumn togglesHeader & FooterSubmenuOn-colorRTLAPI ReferenceDropdownMenuDropdownMenuTriggerDropdownMenuPortalDropdownMenuContentDropdownMenuItemPortalling & collisionDropdownMenuLinkItemDropdownMenuSwitchItemRoundedDropdownMenuArrowDropdownMenuBackdropDropdownMenuHeader / DropdownMenuFooterDropdownMenuLabelDropdownMenuSeparatorDropdownMenuShortcutDropdownMenuSub / SubTrigger / SubContentDropdownMenuCheckboxItemDropdownMenuRadioGroup / RadioItemDropdownMenuSkeletonSizesWidth VariantsWith Icons, Shortcut & DescriptionDestructive ItemsRTLAccessibility