Context menu
A menu of actions on right click or long press, with submenus, checkbox and radio items, and hold feedback on touch.
Last action: Nothing yet
pnpm dlx shadcn@latest add https://hextaui.com/r/context-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/context-menu.tsx Update the import paths to match your project setup.
File list
Give every row its own menu. The open row keeps a highlight, and a destructive item hands off to an alert dialog for confirmation.
Controlled
Pass open and onOpenChange to own the state. The second argument says why it changed, such as trigger-press, outside-press or escape-key. Items with closeOnClick={false} keep it open.
Open: false · last reason: —
Disabled
A disabled <ContextMenu /> gives the area back to the browser’s own menu. Disabled items stay visible but are skipped by the keyboard.
Hold feedback
On touch screens the menu opens after a long press. While the finger is held, the area shrinks slightly so people know the press registered. Moving the finger cancels it. Set holdFeedback={false} to turn it off.
Long content
Long labels wrap inside a 20rem maximum width, and tall menus scroll within the space left in the viewport.
Nested submenus
Submenus open on hover or with the arrow keys, at any depth. A disabled submenu trigger never opens.
Inside a sheet
The menu layers above other overlays, and Escape closes only the menu, not the sheet behind it.
Render as another element
Use render to make the trigger any element, such as a figure, or to turn an item into a link.
Right to left
The menu reads the trigger’s direction, so submenus open to the left and arrow keys flip.
| Key | Action |
|---|---|
| ↓ | 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. In a submenu, only that submenu closes. |
| A–Z | Highlights the next item starting with that letter. |
- A context menu is a shortcut. Make every action in it reachable some other way too, such as a visible button or a dropdown menu, since many people never right click or long press.
- Browsers also fire the context menu event for Shift F10 and the Menu key on a focused element, so a focusable element inside the trigger lets keyboard users open it.
- Shortcuts in
<ContextMenuShortcut />are labels only. Bind the keys yourself. - With reduced motion on, the hold feedback and the item blink are skipped and the menu only fades.
Built on the Base UI context menu. Every part accepts the props of the primitive it wraps, and a function className that receives the part’s state.
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
onOpenChangedetails.reason says what caused the change. | (open: boolean, details) => void | – |
onOpenChangeCompleteRuns after the open or close animation ends. | (open: boolean) => void | – |
disabledShows the browser’s native menu instead. | boolean | false |
loopFocusWrap arrow-key navigation at the ends. | boolean | true |
highlightItemOnHover | boolean | true |
actionsRefClose the menu imperatively. | RefObject<{ close, unmount }> | – |
| Prop | Type | Default |
|---|---|---|
holdFeedbackShrink the area slightly while a long press is held on touch screens. | boolean | true |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-trigger" | Target the trigger in CSS. |
data-popup-open | Present while its menu is open. |
data-holding | Present while a long press is held. |
data-pressed | Present while the trigger is pressed. |
| Prop | Type | Default |
|---|---|---|
sidePreferred side, relative to the pointer. | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | "bottom" |
align | "start" | "center" | "end" | "start" |
sideOffset | number | 0 |
alignOffset | number | 0 |
collisionPaddingSpace to keep between the menu and the viewport edges. | number | { top, right, bottom, left } | – |
collisionAvoidanceHow the menu flips or shifts when it would overflow. | CollisionAvoidance | – |
anchorPosition against something other than the pointer. | Element | VirtualElement | RefObject | – |
finalFocusWhere focus goes after the menu closes. | boolean | RefObject | (closeType) => HTMLElement | boolean | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-content" | The menu popup. |
data-open | Present while open. |
data-starting-style | Present while the menu animates in. |
data-ending-style | Present while the menu animates out. |
data-side | The side it was placed on after collisions. |
data-chosen | Present after an item is clicked. The fade-out waits for the blink. |
--transform-origin | The point the scale animation grows from. |
--available-height | Space left in the viewport. Caps the menu height. |
| Prop | Type | Default |
|---|---|---|
variant | "default" | "destructive" | "default" |
insetIndent the label to line up with checkbox items. | boolean | false |
onClickRuns on click, Enter or Space. The menu closes after a short blink. | (event) => void | – |
closeOnClick | boolean | true |
disabled | boolean | false |
labelText used for typeahead when children are not plain text. | string | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-item" | Target items in CSS. |
data-variant | The current variant. |
data-highlighted | Present on the item under the pointer or keyboard focus. |
data-disabled | Present when the item is disabled. |
data-inset | Present when inset is set. |
data-chosen | Present on the clicked item while it blinks. |
| Prop | Type | Default |
|---|---|---|
checked | boolean | – |
defaultChecked | boolean | false |
onCheckedChange | (checked: boolean, details) => void | – |
closeOnClick | boolean | false |
inset | boolean | false |
disabled | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-checkbox-item" | Target checkbox items in CSS. |
data-checked | Present when checked. |
data-unchecked | Present when unchecked. |
data-highlighted | Present on the item under the pointer or keyboard focus. |
data-disabled | Present when the item is disabled. |
data-inset | Present when inset is set. |
data-chosen | Present on the clicked item while it blinks. |
| Prop | Type | Default |
|---|---|---|
value | any | – |
defaultValue | any | – |
onValueChange | (value: any, details) => void | – |
disabled | boolean | false |
| Prop | Type | Default |
|---|---|---|
value | any | – |
closeOnClick | boolean | false |
inset | boolean | false |
disabled | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-radio-item" | Target radio items in CSS. |
data-checked | Present when selected. |
data-highlighted | Present on the item under the pointer or keyboard focus. |
data-disabled | Present when the item is disabled. |
data-inset | Present when inset is set. |
data-chosen | Present on the clicked item while it blinks. |
| Prop | Type | Default |
|---|---|---|
inset | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
Inside <ContextMenuGroup /> or <ContextMenuRadioGroup /> it labels the group for assistive tech. Elsewhere it is a plain heading.
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
onOpenChange | (open: boolean, details) => void | – |
disabled | boolean | false |
closeParentOnEscClose the whole menu on Escape, not just this submenu. | boolean | false |
| Prop | Type | Default |
|---|---|---|
inset | boolean | false |
openOnHover | boolean | true |
delayMilliseconds of hover before the submenu opens. | number | 100 |
closeDelay | number | 0 |
disabled | boolean | false |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-sub-trigger" | Target submenu triggers in CSS. |
data-popup-open | Present while its submenu is open. |
data-highlighted | Present while highlighted. |
data-disabled | Present when disabled. |
| Prop | Type | Default |
|---|---|---|
sideOpens toward the inline end by default. | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | – |
align | "start" | "center" | "end" | – |
sideOffset | number | 0 |
alignOffset | number | -4 |
collisionPaddingSpace to keep between the menu and the viewport edges. | number | { top, right, bottom, left } | – |
collisionAvoidanceHow the menu flips or shifts when it would overflow. | CollisionAvoidance | – |
anchorPosition against something other than the pointer. | Element | VirtualElement | RefObject | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="context-menu-sub-content" | The submenu popup. Takes the same state attributes as the menu. |
| Prop | Type | Default |
|---|---|---|
dirKeeps shortcuts like ⇧⌘S in order inside right-to-left menus. | "ltr" | "rtl" | "ltr" |
| Attribute | Description |
|---|---|
data-slot="context-menu-shortcut" | The shortcut label. |
<ContextMenuGroup /> groups related items under a label. <ContextMenuSeparator /> draws a divider. Both accept render and className, and carry context-menu-group and context-menu-separator as their data-slot.