Skip to content

Dropdown component

Dropdown is a compound menu component: Dropdown owns the open state and positioning, DropdownTrigger wraps whatever element opens it, and DropdownMenu holds DropdownItems (or DropdownSections of them). It supports single/multiple selection, nested submenus, and opt-in virtualization for long lists, on top of full keyboard navigation and typeahead.

Navigation55 propsAccessibility notes

Playground

Change a prop on the right and the preview updates. Switch to Code to copy what you see.

Recipes

Copy-paste patterns for the ways Dropdown actually gets used — each one a real, self-contained example.

Action menu

The compound shape: DropdownTrigger wraps the opener, DropdownMenu holds the items. onAction fires with the activated item's key — usually all you need.

<Dropdown>
  <DropdownTrigger>
    <Button variant="bordered">Actions</Button>
  </DropdownTrigger>
  <DropdownMenu aria-label="Actions" onAction={(key) => console.log(key)}>
    <DropdownItem key="edit">Edit</DropdownItem>
    <DropdownItem key="duplicate">Duplicate</DropdownItem>
    <DropdownItem key="archive">Archive</DropdownItem>
  </DropdownMenu>
</Dropdown>

Icons and shortcuts

startContent puts an icon before the label; shortcut renders a keyboard hint aligned to the right.

<DropdownMenu aria-label="Edit menu" onAction={(key) => console.log(key)}>
  <DropdownItem key="copy" startContent={<CopyIcon />} shortcut="⌘C">
    Copy
  </DropdownItem>
  <DropdownItem key="paste" startContent={<ClipboardIcon />} shortcut="⌘V">
    Paste
  </DropdownItem>
</DropdownMenu>

Sections with a divider

Group related items under a DropdownSection with a title; showDivider separates a section (like a destructive one) from the rest.

<DropdownMenu aria-label="Profile menu" onAction={(key) => console.log(key)}>
  <DropdownSection title="Account" showDivider>
    <DropdownItem key="profile">Profile</DropdownItem>
    <DropdownItem key="settings">Settings</DropdownItem>
  </DropdownSection>
  <DropdownSection title="Danger zone">
    <DropdownItem key="signout">
      <span className="text-danger">Sign out</span>
    </DropdownItem>
  </DropdownSection>
</DropdownMenu>

Single-select menu

selectionMode="single" makes the menu track a choice — pair with selectedKeys/onSelectionChange to control it. A checkmark marks the active item.

const [sort, setSort] = useState("name");

<DropdownMenu
  aria-label="Sort by"
  selectionMode="single"
  selectedKeys={[sort]}
  onSelectionChange={(keys) => setSort([...keys][0])}
>
  <DropdownItem key="name">Name</DropdownItem>
  <DropdownItem key="date">Date created</DropdownItem>
  <DropdownItem key="size">Size</DropdownItem>
</DropdownMenu>

Disabled item

disabledKeys (or isDisabled on an item) removes an option from keyboard navigation, not just dims it.

<DropdownMenu aria-label="Actions" disabledKeys={["publish"]}>
  <DropdownItem key="save">Save draft</DropdownItem>
  <DropdownItem key="publish">Publish (needs review)</DropdownItem>
  <DropdownItem key="delete">
    <span className="text-danger">Delete</span>
  </DropdownItem>
</DropdownMenu>

The compound shape

DropdownTrigger takes exactly one child — the element that opens the menu on press — and DropdownMenu holds the items. onAction fires with the activated item's key; that's usually all you need instead of an onClick per item.

<Dropdown>
  <DropdownTrigger>
    <Button variant="bordered">Actions</Button>
  </DropdownTrigger>
  <DropdownMenu aria-label="Actions" onAction={(key) => handleAction(String(key))}>
    <DropdownItem key="edit">Edit</DropdownItem>
    <DropdownItem key="duplicate">Duplicate</DropdownItem>
    <DropdownItem key="delete" className="text-danger">Delete</DropdownItem>
  </DropdownMenu>
</Dropdown>

Selection modes

selectionMode defaults to "none" (a plain action menu). Set it to "single" or "multiple" for a menu that tracks its own selected item(s) — pair with selectedKeys/onSelectionChange to control it, or defaultSelectedKeys to just seed the initial state and let Dropdown manage it from there.

<DropdownMenu
  aria-label="Sort by"
  selectionMode="single"
  selectedKeys={[sortKey]}
  onSelectionChange={(keys) => setSortKey([...keys][0] as string)}
>
  <DropdownItem key="name">Name</DropdownItem>
  <DropdownItem key="date">Date</DropdownItem>
</DropdownMenu>

Nested submenus

An item with its own DropdownMenu inside it becomes a submenu, opened on hover or ArrowRight — submenuPlacement controls which side it opens on when the default (right-start) would run out of viewport space.

<DropdownItem key="export">
  Export
  <DropdownMenu aria-label="Export format">
    <DropdownItem key="csv">CSV</DropdownItem>
    <DropdownItem key="json">JSON</DropdownItem>
  </DropdownMenu>
</DropdownItem>

Long lists: opt-in virtualization

For a menu with hundreds of items (a searchable list of every country, every user in a workspace), pass virtualization={{ itemHeight }} on DropdownMenu so only the visible slice of items actually renders to the DOM. Leave it off for ordinary menus — it's specifically for lists long enough that full rendering would be the bottleneck.

<DropdownMenu aria-label="Country" items={countries} virtualization={{ itemHeight: 36 }}>
  {(country) => <DropdownItem key={country.code}>{country.name}</DropdownItem>}
</DropdownMenu>

Positioning and dismissal

placement (12 directional options) plus shouldFlip (on by default, so the menu flips to the opposite side automatically when it would otherwise run off-screen) control layout; isDismissable/isKeyboardDismissDisabled/shouldCloseOnInteractOutside control how it can be closed, and closeOnSelect (on by default) decides whether picking an item closes the menu — turn it off for a multi-select menu you want to stay open across several picks.

Props

The full public API for Dropdown. Every prop is stable and intentionally typed.

Dropdown — Content

children

ReactNode

default: —

A DropdownTrigger and a DropdownMenu.

Dropdown — Layout & positioning

placement

"bottom-start" | "bottom-end" | "top-start" | ... (12-way)

default: "bottom-start"

Menu position relative to the trigger.

offset

number

default: 8

Distance from the trigger.

containerPadding

number

default: 8

Minimum distance kept from the viewport edge.

shouldFlip

boolean

default: true

Flips placement to stay in the viewport.

portalContainer

Element | DocumentFragment | null

default: —

Custom portal mount target.

Dropdown — Behavior & states

isOpen

boolean

default: —

Controlled open state.

defaultOpen

boolean

default: false

Uncontrolled initial open state.

isDismissable

boolean

default: true

Allows dismissing via outside click/Escape.

isKeyboardDismissDisabled

boolean

default: false

Disables closing via Escape.

shouldCloseOnInteractOutside

(target: Element) => boolean

default: —

Custom predicate for outside-interaction dismissal.

closeOnSelect

boolean

default: true

Closes the menu after an item is selected.

Dropdown — Events

onOpenChange

(isOpen: boolean) => void

default: —

Called when open state changes.

Dropdown — Styling

classNames

Partial<Record<"base" | "content", string>>

default: —

Per-slot class overrides.

className

string

default: —

Class applied to the root.

style

CSSProperties

default: —

Inline styles for the root.

DropdownTrigger — Content

children

ReactElement

default: —

The single element that opens the menu on press (required).

DropdownMenu — Content

children

ReactNode | ((item: T) => ReactElement)

default: —

Declarative DropdownItem/DropdownSection children, or a render function with items.

items

Iterable<T>

default: —

Dynamic items, used with a render-function children.

topContent / bottomContent

ReactNode

default: —

Content rendered above/below the item list.

emptyContent

ReactNode

default: "No items."

Shown when there are no items.

DropdownMenu — Appearance

variant

"solid" | "bordered" | "light" | "flat" | "faded" | "shadow"

default: solid

Visual style variant.

color

DropdownColor

default: default

Semantic color.

hideSelectedIcon

boolean

default: false

Hides the selected-item checkmark.

DropdownMenu — Behavior & states

selectionMode

"none" | "single" | "multiple"

default: none

Item selection behavior.

selectedKeys

"all" | Iterable<Key>

default: —

Controlled selected keys.

defaultSelectedKeys

"all" | Iterable<Key>

default: —

Uncontrolled initial selected keys.

disabledKeys

Iterable<Key>

default: —

Keys of items to disable.

disallowEmptySelection

boolean

default: false

Prevents deselecting the last selected item.

autoFocus

boolean | "first" | "last"

default: false

Focuses an item automatically when the menu opens.

hideEmptyContent

boolean

default: false

Hides the empty-state content entirely.

shouldFocusWrap

boolean

default: false

Wraps keyboard focus from the last item to the first.

closeOnSelect

boolean

default: parent Dropdown's value

Per-menu override of the root's closeOnSelect.

virtualization

{ itemHeight: number; overscan?: number }

default: —

Enables fixed-height virtualized rendering for long lists.

DropdownMenu — Motion

disableAnimation

boolean

default: false

Disables the menu's open animation.

DropdownMenu — Events

onAction

(key: Key) => void

default: —

Called when an item is activated.

onSelectionChange

(keys) => void

default: —

Called when selection changes.

onClose

() => void

default: —

Called when the menu requests to close.

DropdownMenu — Styling

classNames

DropdownMenuClassNames

default: —

Per-slot class overrides (base/list/emptyContent).

itemClasses

Partial<Record<DropdownItemSlot, string>>

default: —

Per-slot class overrides applied to every item.

DropdownItem — Content

children / title

ReactNode

default: —

Item label.

textValue

string

default: —

Plain-text override for typeahead/accessible naming.

description

ReactNode

default: —

Secondary text below the label.

shortcut

ReactNode

default: —

Keyboard shortcut hint.

startContent / endContent

ReactNode

default: —

Content rendered before/after the label.

selectedIcon

ReactNode

default: —

Overrides the default selected checkmark.

DropdownItem — Layout & positioning

submenuPlacement

DropdownPlacement

default: "right-start"

Position of a nested DropdownMenu, when this item has one.

DropdownItem — Behavior & states

showDivider

boolean

default: false

Shows a divider below this item.

href / target / rel / download / ping / referrerPolicy

string

default: —

Renders the item as a link when href is set.

isDisabled

boolean

default: false

Disables this item.

isReadOnly

boolean

default: false

Not selectable, but not visually disabled.

isSelected

boolean

default: —

Marks this item as selected. Use it when selection is managed outside the menu; otherwise the menu's selectedKeys decide.

closeOnSelect

boolean

default: —

Per-item override of the menu's closeOnSelect.

itemKey

Key

default: —

Explicit key, when not using items+render-function.

DropdownItem — Events

onAction / onClose / onClick / onPress / onPressStart / onPressEnd / onPressChange / onPressUp / onKeyDown / onKeyUp

callback

default: —

Interaction callbacks.

Accessibility

  • Implements the full menu/menuitem ARIA pattern with roving tabindex, arrow-key navigation, and typeahead (typing jumps to the next matching item).
  • When an item's visible label is a non-text node (an icon plus a badge, say), pass textValue with the plain-text equivalent so typeahead and the accessible name both still work correctly.
  • Always give DropdownMenu its own aria-label (or aria-labelledby) — the trigger button's own label doesn't automatically describe what the opened menu contains.
  • isReadOnly items are visible and reachable by keyboard but not selectable — different from isDisabled, which removes them from keyboard navigation entirely.

Best practices

  • Keep selectionMode="none" for plain action menus (Edit/Duplicate/Delete) — only reach for "single"/"multiple" when the menu is genuinely representing a choice, not a set of one-shot actions.
  • Turn closeOnSelect off for multi-select menus where picking several items in one sitting is the expected flow; leave it on for single actions, where staying open after a pick just adds an extra dismiss step.
  • Reserve virtualization for lists long enough to actually need it (order of hundreds of items) — for a typical 5-15 item menu it's unnecessary overhead.
  • Use showDivider on an item to separate a destructive action (Delete) from the rest of the menu, rather than relying on color alone to signal it's different.