Skip to content
Redpanda UI
Esc
↑↓navigate↵open⌘Jpreview
Made by imskyleen
Edit on GitHub

Dropdown Menu

Displays a menu of actions or functions when the user presses a trigger button, with groups, submenus, checkbox and radio items, and shortcuts.

Installation

bash
npx shadcn@latest add @redpanda/dropdown-menu
bash
pnpm dlx shadcn@latest add @redpanda/dropdown-menu
bash
yarn dlx shadcn@latest add @redpanda/dropdown-menu
bash
bunx shadcn@latest add @redpanda/dropdown-menu
bash
nubx shadcn@latest add @redpanda/dropdown-menu
bash
aube dlx shadcn@latest add @redpanda/dropdown-menu

When to use

Use this decision tree to determine when to use the Dropdown Menu component:

Usage

tsx
<DropdownMenu>  <DropdownMenuTrigger>Open Dropdown Menu</DropdownMenuTrigger>  <DropdownMenuContent>    <DropdownMenuItem>Item 1</DropdownMenuItem>    <DropdownMenuItem>Item 2</DropdownMenuItem>    <DropdownMenuItem>Item 3</DropdownMenuItem>  </DropdownMenuContent></DropdownMenu>

Examples

Item types

Checkboxes

Radio group

Use DropdownMenuLinkItem href="..." for navigation rather than nesting an anchor inside an action item. The actual menu item is the native link, preserving its URL, browser modifiers, ref, and keyboard navigation. Set aria-current="page" on the current link; the shared menu-row recipe owns its persistent appearance. Link items preserve Base UI’s closeOnClick={false} default. Use closeOnClick when navigation should close the current menu, such as the in-page project-summary example below. Links do not have a disabled action API.

Keyboard shortcuts

Composition

Labeled sections

Filtering

Long menus can take a search field. Wrap the DropdownMenu in a DropdownMenuFilterProvider, add a DropdownMenuInput to the content, and put the items in a DropdownMenuList. Typing narrows the items; use the arrow keys to highlight a match and Enter to activate it. DropdownMenuEmpty shows (and is announced) once nothing matches.

tsx
<DropdownMenuFilterProvider>  <DropdownMenu>    <DropdownMenuTrigger render={<Button variant="outline" />}>Add component</DropdownMenuTrigger>    <DropdownMenuContent className="w-64">      <DropdownMenuInput aria-label="Search components" placeholder="Search components" />      <DropdownMenuEmpty>No matching components.</DropdownMenuEmpty>      <DropdownMenuList>        <DropdownMenuGroup>          <DropdownMenuLabel>Inputs</DropdownMenuLabel>          <DropdownMenuItem>Kafka</DropdownMenuItem>          <DropdownMenuItem>HTTP server</DropdownMenuItem>        </DropdownMenuGroup>      </DropdownMenuList>    </DropdownMenuContent>  </DropdownMenu></DropdownMenuFilterProvider>
  • Name the input. Give DropdownMenuInput an aria-label, an aria-labelledby, or a visible <label>.
  • Automatic highlighting. Opt in with autoHighlight to highlight the first match while typing. By default, arrow-key navigation passes through the input at either end of the list, showing its focus ring. Enabling autoHighlight skips that input step and wraps directly between items.
  • What matches. An item matches when its text contains the query, ignoring case, accents and punctuation. Its text includes everything it renders, so set label on items with extra text such as a DropdownMenuShortcut: <DropdownMenuItem label="Duplicate">Duplicate <DropdownMenuShortcut>⌘D</DropdownMenuShortcut></DropdownMenuItem>.
  • Groups and separators. A DropdownMenuGroup whose items all stop matching hides along with its DropdownMenuLabel, so keep labels inside groups. Separators hide while a query is typed and come back once it’s cleared.
  • Submenus. Wrap each DropdownMenuSub you want searchable in its own DropdownMenuFilterProvider and give its DropdownMenuSubContent an input and a list. Submenus without a provider stay unfiltered.
  • Controlling the query. value and onValueChange on the provider control the query. It resets to an empty string when the menu closes, reported with the popup-close reason; call eventDetails.cancel() to keep it.
  • Custom matching. Pass filter to replace the matching (Menu.useFilter() from @base-ui/react/menu gives the same locale-aware contains and startsWith), or filter={null} to filter the items yourself, for example from a server.

Migrating from automatic highlighting

DropdownMenuFilterProvider previously defaulted autoHighlight to true; it now defaults to false. Typing filters the list without highlighting an item, so press an arrow key before Enter to activate a match. To preserve the previous type-then-Enter behavior, pass autoHighlight explicitly:

tsx
<DropdownMenuFilterProvider autoHighlight>{/* Existing menu, input, and list */}</DropdownMenuFilterProvider>

Triggers

The example uses stable project-navigation destination IDs, so bookmarks and modified-click navigation resolve in a fresh page too. When rendering multiple copies, pass a distinct, stable id to each DropdownMenuLinkTriggerDemo, such as primary-project-navigation and secondary-project-navigation. Do not generate bookmark destinations with useId: those identities depend on the rendered tree. Normal fragment navigation stays on the current page and focuses the native destination; no custom navigation or focus handler is needed.

Avatar user menu

Opening dialogs from menu items

A menu item that opens a Dialog, AlertDialog, or Sheet is a common pattern but a nuanced composition. The recommended shape: render the dialog as a sibling of DropdownMenu and share open state via consumer state. Avoid rendering the dialog as a descendant of DropdownMenuContent — the menu’s portal unmounts on close and tears the dialog down before it reaches a stable open state.

Closing a menu can unmount its descendants before a nested dialog has completed its own lifetime. Keep the owner mounted, or put the dialog outside the closing menu. Do not compose a dialog trigger that changes a menu item into a button; use the menu item’s onClick to change controlled dialog state.

The demo shows two supported compositions:

  1. Keep-mounted descendant — <DropdownMenuContent keepMounted> retains its subtree when the menu closes, allowing the nested dialog to remain open. Prefer the sibling pattern when retaining that DOM is unnecessary. Pass the menu-trigger ref to the dialog popup’s finalFocus, because the invoking menu item is hidden after the menu closes.
  2. Recommended sibling — the dialog is a sibling of DropdownMenu, opened from a real menu item’s onClick through shared state. The menu’s portal can unmount independently.

Animation

Open on hover

Props

Props

PropType
autoHighlight?boolean | 'always'

Highlight the first matching item while filtering, so Enter activates it. `'always'` highlights it even when the query is empty. Enabling this skips the input in the arrow-key loop.

Typeboolean | 'always'
Defaultfalse
defaultValue?string

The query when the menu first renders. To control it, use `value` instead.

Typestring
Default''
value?string

The query, when controlled. Reset to an empty string when the menu closes unless you cancel that change.

Typestring
onValueChange?(value: string, eventDetails: Menu.FilterProvider.ChangeEventDetails) => void

Called when the query changes. `eventDetails.reason` is `'input-change'`, `'input-clear'`, `'clear-press'` or `'popup-close'`.

Type(value: string, eventDetails: Menu.FilterProvider.ChangeEventDetails) => void
filter?((text: string, query: string) => boolean) | null

Replaces the matching. Receives each item’s `label` (or rendered text) and the trimmed query. Pass `null` to render filtered items yourself.

Type((text: string, query: string) => boolean) | null
locale?Intl.LocalesArgument

The locale the default matching compares strings in. Defaults to the user’s runtime locale.

TypeIntl.LocalesArgument

DropdownMenuInput, DropdownMenuEmpty and DropdownMenuList take their Base UI part’s props (Input, Empty, List), plus testId on the input and the empty state. DropdownMenuInput and DropdownMenuEmpty need a DropdownMenuFilterProvider; DropdownMenuList also works in a plain menu.

PropType
inset?boolean

Whether the dropdown menu sub trigger is inset

Typeboolean
Defaultfalse
PropType
keepMounted?boolean

Keep the menu's portal subtree mounted across close cycles. Use when state inside the menu must outlive close (open dialogs, in-flight confirmations). Trades the close animation for survival of the subtree. Avoid on lists with many rows × menu-per-row — prefer rendering dialogs as siblings of DropdownMenu instead.

Typeboolean
Defaultfalse

Use action items for commands, and DropdownMenuLinkItem for navigation. An action item’s advertised render prop is forwarded to Base UI, preserving its rendered host, refs, state callbacks, and handlers.

PropType
inset?boolean

Whether the dropdown menu item is inset

Typeboolean
Defaultfalse
padding?'standard' | 'comfortable'

Standard menu-row padding, or comfortable padding for icon and avatar rows

Type'standard' | 'comfortable'
Defaultstandard
variant?'default' | 'destructive'

The variant of the dropdown menu item

Type'default' | 'destructive'
Defaultdefault

Accepts native anchor props and the installed Base UI Menu.LinkItem composition API, plus the same inset and padding inputs as DropdownMenuItem. The default padding is standard; comfortable provides the owned spacing for avatar and icon rows. The default closeOnClick is false.

PropType
variant?'default' | 'muted' | 'identity'

Default section heading, muted small heading, or an unpadded identity header

Type'default' | 'muted' | 'identity'
Defaultdefault
inset?boolean

Whether the dropdown menu label is inset

Typeboolean
Defaultfalse

Keyboard interactions

Key Action
Enter / Space / ArrowDown on DropdownMenuTrigger Opens the menu and focuses the first item.
ArrowUp / ArrowDown Moves between items. Wraps at the edges.
ArrowLeft / ArrowRight Opens / closes submenus.
Home / End Jumps to first / last item.
Enter / Space Activates the focused item.
Escape Closes the menu and restores focus to the trigger.
Typing a character Moves focus to the next item whose label starts with that character (typeahead).

In a filterable menu, opening it with a click or the keyboard focuses DropdownMenuInput, and typing filters instead of jumping by typeahead. ArrowUp / ArrowDown move the highlight while the input keeps focus, Enter activates the highlighted item, Tab closes the menu, and Shift + Tab returns focus to the trigger. Opening on hover or touch leaves the input unfocused, so the on-screen keyboard stays hidden.

By default, pressing ArrowDown after the last item or ArrowUp before the first returns the highlight to the input, where a focus ring marks the editing step. Home / End move the caret when the query has text; with an empty query and a highlighted item, they jump to the first / last item. Modified arrow keys keep their native text-editing behavior.

Accessibility

  • DropdownMenuTrigger exposes aria-haspopup="menu" and aria-expanded; the menu itself is role="menu" with role="menuitem" / menuitemcheckbox / menuitemradio on its children.
  • Focus is fully roving inside the menu — no tabIndex={0} needed on items.
  • A filterable menu’s popup is a dialog that holds the searchbox input and the menu list, and its trigger exposes aria-haspopup="dialog". DropdownMenuEmpty is a polite live region. The clear button is pointer-only: it’s hidden from assistive tech and the tab order, since deleting the text does the same.
  • Destructive items should set variant="destructive" (visual) and, if the action is irreversible, open an AlertDialog for confirmation rather than firing immediately.
  • The trigger must be a button (or another interactive element via the render prop); static <div> triggers lose keyboard activation.

Controlled vs uncontrolled

Uncontrolled — typical case:

tsx
<DropdownMenu>  <DropdownMenuTrigger render={<Button onClick={() => undefined}>Actions</Button>} />  <DropdownMenuContent>    <DropdownMenuItem onClick={onEdit}>Edit</DropdownMenuItem>    <DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem>  </DropdownMenuContent></DropdownMenu>

Controlled — drive open from parent state (e.g. to open programmatically from a keyboard shortcut):

tsx
const [open, setOpen] = useState(false);
<DropdownMenu open={open} onOpenChange={setOpen}>  <DropdownMenuTrigger render={<Button onClick={() => undefined}>Actions</Button>} />  <DropdownMenuContent>{/* ... */}</DropdownMenuContent></DropdownMenu>;

Credits

  • We use Base UI for the dropdown menu component.
  • We take our inspiration from Shadcn UI for the dropdown menu style.

Use variant="sidebar" on DropdownMenuContent for organization, account, and project menus in a sidebar. The owned recipe provides their larger corner radius. Use variant="destructive" on destructive DropdownMenuItem actions rather than overriding their text color; the recipe also owns highlighted and icon states.

Recent changes

  • patchv1.2.0Pin shipped dependency floors to the version we develop against. Registry items now declare ranges like `^5.1.9` (the actual installed version) instead of collapsing to `^5.0.0`, so consumers start on the known-tested baseline while caret semantics still allow any compatible release within the same major.#133
  • minorv1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
  • minorv1.1.0Bulk install experience and Dialog polish.#119
  • minorv1.0.0Post-Base-UI polish. Public API unchanged.#116
  • majorv1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114
See full history →

Last updated on

Was this page helpful?