Dropdown menu
A menu of actions and options behind a button, with groups, submenus, checkbox and radio items, and shortcuts.
pnpm dlx shadcn@latest add https://hextaui.com/r/dropdown-menu.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/dropdown-menu.tsx Update the import paths to match your project setup.
Safe area
Moving diagonally from a submenu trigger toward its submenu crosses sibling triggers. The safe area keeps the submenu open while the pointer heads into it. Turn on showSafeArea to see it live: the dot turns green inside the area and red where the submenu would switch.
Checkboxes
<DropdownMenuCheckboxItem /> toggles a setting and keeps the menu open, so several can be changed in a row. The check draws in and out instead of popping.
Radio group
Wrap <DropdownMenuRadioItem /> in a <DropdownMenuRadioGroup /> to pick one option. A label inside the group names it for screen readers.
Select-like
The menu is never narrower than its trigger. Set closeOnClick on radio items when picking one should also close the menu.
Row actions
One menu serves every row. Create a handle with createDropdownMenuHandle, pass it to each trigger with a payload, and read the payload in the menu. Delete hands off to an alert dialog.
- Quarterly report.pdf
- Brand assets
- Roadmap 2027.md
Controlled
Pass open and onOpenChange. The second argument says why the menu changed, such as a trigger press, an item press or Escape. Items with closeOnClick={false} keep it open.
Open: false · last reason: none
Open on hover
openOnHover on the trigger opens the menu after delay and closes it after closeDelay. Clicking and the keyboard still work, so touch and keyboard users aren’t locked out.
Disabled
A disabled trigger never opens. Disabled items stay visible and are skipped by the arrow keys.
Long content
Labels wrap instead of stretching the menu past 20rem, unbroken strings break anywhere, and shortcuts stay on the first line.
Scrolling
When there isn’t room for every item, the menu scrolls inside the viewport and the highlighted item scrolls into view.
Inside a sheet
The menu layers above the sheet. Escape closes the menu first, then the sheet.
Right to left
The menu, submenu chevron and submenu side follow the trigger’s direction. Shortcuts keep their left-to-right order.
| Key | Action |
|---|---|
| EnterSpace↓ | On the trigger, opens the menu and highlights the first item. |
| ↓ | Highlights the next item, wrapping at the end. |
| ↑ | Highlights the previous item, wrapping at the start. |
| Home | Highlights the first item. |
| End | Highlights the last item. |
| EnterSpace | Runs the highlighted item. Checkbox and radio items toggle and keep the menu open. |
| → | Opens the highlighted submenu and moves into it. ← in right-to-left layouts. |
| ← | Closes the current submenu and returns to its trigger. → in right-to-left layouts. |
| Esc | Closes the current menu and returns focus to its trigger. In a submenu, only that submenu closes. |
| A–Z | Highlights the next item starting with that letter. |
- The trigger is announced as a menu button, and focus returns to it when the menu closes.
- Give icon-only triggers an
aria-label, like the row actions example does. <DropdownMenuShortcut />is a visual label only. Bind the keys yourself.- With reduced motion on, the menu fades without scaling and the item blink is skipped.
Built on the Base UI menu. Every part accepts the props of the primitive it wraps.
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
showSafeAreaDraws the submenu safe area while the menu is open. For debugging and demos. | boolean | false |
onOpenChangedetails.reason says what caused the change. | (open: boolean, details) => void | – |
modalLocks page scroll and blocks outside clicks while open. | boolean | true |
loopFocusWrap the arrow keys at either end. | boolean | true |
handleConnects detached triggers to this menu. | DropdownMenuHandle<Payload> | – |
childrenUse the function form to read the opening trigger’s payload. | ReactNode | ({ payload }) => ReactNode | – |
disabled | boolean | false |
| Prop | Type | Default |
|---|---|---|
openOnHover | boolean | false |
delayMilliseconds before a hover opens the menu. | number | 100 |
closeDelayMilliseconds before a hover-opened menu closes. | number | 0 |
handle | DropdownMenuHandle<Payload> | – |
payloadPassed to the menu when this trigger opens it. | Payload | – |
disabled | boolean | false |
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-trigger" | Target the trigger in CSS. |
data-popup-open | Present while its menu is open. |
data-pressed | Present while the trigger is pressed. |
data-disabled | Present when the trigger is disabled. |
| Prop | Type | Default |
|---|---|---|
side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | "bottom" |
align | "start" | "center" | "end" | "start" |
sideOffset | number | 4 |
alignOffset | number | 0 |
collisionPaddingSpace kept between the menu and the edge of the viewport. | number | Rect | 8 |
anchorPosition against another element instead of the trigger. | Element | RefObject | VirtualElement | – |
sticky | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-content" | Target the menu in CSS. |
data-open | Present while the menu is open. |
data-starting-style | Present while the menu animates in. |
data-ending-style | Present while the menu animates out. |
data-side | The side the menu settled on after collision handling. |
data-align | The alignment it settled on. |
data-chosen | Present after an item is clicked, so the menu waits for the item’s confirmation blink before closing. |
--anchor-width | The trigger’s width. The menu is at least this wide. |
--available-height | Room left in the viewport. Long menus scroll within it. |
--transform-origin | Where the scale animation grows from, next to the trigger. |
| Prop | Type | Default |
|---|---|---|
variant | "default" | "destructive" | "default" |
insetIndents the item to line up with checkbox items. | boolean | false |
closeOnClick | boolean | true |
onClick | (event) => void | – |
labelText used for type-ahead when children aren’t plain text. | string | – |
disabled | boolean | false |
renderRender a link with render={<a href="…" />}. | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-item" | Target items in CSS. |
data-variant | The current variant. |
data-highlighted | Present while the item is highlighted by pointer or keyboard. |
data-disabled | Present when the item is disabled. |
data-chosen | Present on the item that was just clicked, while it blinks. |
data-inset | Present when inset is set. |
| Prop | Type | Default |
|---|---|---|
checked | boolean | – |
defaultChecked | boolean | false |
onCheckedChange | (checked: boolean, details) => void | – |
closeOnClick | boolean | false |
inset | boolean | false |
disabled | boolean | false |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-checkbox-item" | Target checkbox items in CSS. |
data-checked | Present when checked. |
data-unchecked | Present when not checked. |
data-highlighted | Present while the item is highlighted by pointer or keyboard. |
data-disabled | Present when the item is disabled. |
data-chosen | Present on the item that was just clicked, while it blinks. |
data-inset | Present when inset is set. |
| Prop | Type | Default |
|---|---|---|
value | Value | – |
defaultValue | Value | – |
onValueChange | (value: Value, details) => void | – |
disabled | boolean | false |
| Prop | Type | Default |
|---|---|---|
value | Value | – |
closeOnClick | boolean | false |
inset | boolean | false |
disabled | boolean | false |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-radio-item" | Target radio items in CSS. |
data-checked | Present on the selected item. |
data-unchecked | Present on the other items. |
data-highlighted | Present while the item is highlighted by pointer or keyboard. |
data-disabled | Present when the item is disabled. |
data-chosen | Present on the item that was just clicked, while it blinks. |
data-inset | Present when inset is set. |
Groups related items. A <DropdownMenuLabel /> inside it becomes the group’s accessible name.
| Prop | Type | Default |
|---|---|---|
inset | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
A divider between groups, announced as a separator.
| Prop | Type | Default |
|---|---|---|
dirShortcuts keep their key order in right-to-left menus. | "ltr" | "rtl" | "ltr" |
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
onOpenChange | (open: boolean, details) => void | – |
closeParentOnEscClose the whole menu, not just this submenu, on Escape. | boolean | false |
| Prop | Type | Default |
|---|---|---|
inset | boolean | false |
disabled | boolean | false |
delayMilliseconds before hovering opens the submenu. | number | 100 |
closeDelay | number | 0 |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-sub-trigger" | Target submenu triggers in CSS. |
data-popup-open | Present while its submenu is open. |
data-highlighted | Present while highlighted. |
| Prop | Type | Default |
|---|---|---|
sideOffset | number | 0 |
alignOffset | number | -4 |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="dropdown-menu-sub-content" | Target submenus in CSS. |
data-open | Present while the menu is open. |
data-starting-style | Present while the menu animates in. |
data-ending-style | Present while the menu animates out. |
data-side | The side the menu settled on after collision handling. |
data-align | The alignment it settled on. |
data-chosen | Present after an item is clicked, so the menu waits for the item’s confirmation blink before closing. |
--available-height | Room left in the viewport. Long menus scroll within it. |
--transform-origin | Where the scale animation grows from, next to the trigger. |