A menu opened by right-clicking an area.
Right click here
Back
Forward
Reload
Save page as…
Print…
Inspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/lib/components/ui/context-menu";
export default function ContextMenuDemo() {
return ContextMenu(
ContextMenuTrigger(
{
class:
"flex h-36 w-full max-w-sm items-center justify-center rounded-md border border-dashed text-sm select-none",
},
"Right click here",
),
ContextMenuContent(
{ class: "w-52" },
ContextMenuItem({ onSelect: () => console.log("back") }, "Back"),
ContextMenuItem({ disabled: true }, "Forward"),
ContextMenuItem({ onSelect: () => console.log("reload") }, "Reload"),
ContextMenuSeparator(),
ContextMenuSub(
ContextMenuSubTrigger("Share"),
ContextMenuSubContent(
{ class: "w-44" },
ContextMenuItem({ onSelect: () => console.log("share-email") }, "Email"),
ContextMenuItem({ onSelect: () => console.log("share-message") }, "Message"),
ContextMenuItem({ onSelect: () => console.log("share-link") }, "Copy link"),
),
),
ContextMenuSeparator(),
ContextMenuItem({ onSelect: () => console.log("save") }, "Save page as…"),
ContextMenuItem({ onSelect: () => console.log("print") }, "Print…"),
ContextMenuSeparator(),
ContextMenuItem({ onSelect: () => console.log("inspect") }, "Inspect"),
),
);
}
A context menu opens where the pointer is when an area is right-clicked (or long-pressed on touch). ContextMenu is the root, ContextMenuTrigger the area, and ContextMenuContent the panel. It shares its content, items, and keyboard model with Dropdown Menu and Menubar — only how the menu opens differs.
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@implementjs/primitives";
ContextMenu(
ContextMenuTrigger({ class: "block h-36 rounded-md border border-dashed" }, "Right click here"),
ContextMenuContent(
ContextMenuItem({ onSelect: () => reload() }, "Reload"),
ContextMenuItem({ onSelect: () => inspect() }, "Inspect"),
),
);
Each part accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent.
Opening
The trigger intercepts the contextmenu event, so the browser menu is replaced inside the area. The panel anchors to the pointer position — right-clicking somewhere else while open moves it there. On touch, a long press (700ms) opens it. Pass disabled on the trigger to fall back to the native browser menu. While open, the page behind cannot scroll; pass preventScroll: false on the root to leave it scrollable.
Items, structure, and keyboard
Everything inside the panel is the shared menu set: ContextMenuItem with onSelect and closeOnSelect, ContextMenuCheckboxItem, ContextMenuRadioGroup and ContextMenuRadioItem, ContextMenuGroup with ContextMenuGroupHeading, ContextMenuSeparator, and nested submenus via ContextMenuSub, ContextMenuSubTrigger, and ContextMenuSubContent. The keyboard model matches the dropdown menu: arrows move, typing jumps, Enter and Space activate, ArrowRight and ArrowLeft enter and leave submenus, Escape closes.
Styling
The primitive does not hide the closed panel — style ContextMenuContent against data-state, and items against data-highlighted and data-disabled. Every part sets a data-context-menu-* attribute, and the trigger exposes data-state so the area itself can react while the menu is open.
API Reference
The root. Owns whether the menu is open and provides that to the parts inside it.
| Prop | Type | Default | Description |
|---|
open | Signal<boolean> | boolean | false | The open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state. |
preventScroll | boolean | true | When true, the page behind cannot scroll while the menu is open. The panel can still scroll if you give it overflow. |
The area the menu belongs to. Right-clicking (or long-pressing on touch) opens the menu at the pointer. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
disabled | Signal<boolean> | boolean | false | Falls back to the native browser menu. |
| Data attribute | Value |
|---|
[data-context-menu-trigger] | Present |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
The floating panel of items. Sets role="menu". Style it against data-state and data-side; the primitive does not hide it for you. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
side | "top" | "bottom" | "left" | "right" | "bottom" | Preferred side of the anchor to place the panel. |
align | "start" | "center" | "end" | "start" | How the panel aligns along the chosen side. |
offset | number | 0 | Distance in pixels between the anchor and the panel. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
| Data attribute | Value |
|---|
[data-context-menu-content] | Present |
[data-state] | "open" | "closed" |
[data-side] | "top" | "bottom" | "left" | "right" |
[data-align] | "start" | "center" | "end" |
One action. Sets role="menuitem"; focus follows the pointer. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
onSelect | () => void | — | Runs when the item is activated with a click, Enter, or Space. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|
[data-context-menu-item] | Present |
[data-highlighted] | Present while focused |
[data-disabled] | Present when disabled |
An item holding a checked state. Sets role="menuitemcheckbox" and aria-checked. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
checked | Signal<boolean> | boolean | false | The checked state; selecting toggles it. Pass a signal to control it from outside. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|
[data-context-menu-checkbox-item] | Present |
[data-state] | "checked" | "unchecked" |
[data-highlighted] | Present while focused |
[data-disabled] | Present when disabled |
Wraps radio items and owns which one is checked. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
value | Signal<string | null> | string | null | null | The checked item. Pass a signal to control it from outside. |
| Data attribute | Value |
|---|
[data-context-menu-radio-group] | Present |
One choice in a radio group. Sets role="menuitemradio" and aria-checked. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
value* | string | — | Identifies the item. Must be unique within the radio group. |
closeOnSelect | boolean | true | Whether selecting the item closes the menu. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the item; the keyboard skips it. |
| Data attribute | Value |
|---|
[data-context-menu-radio-item] | Present |
[data-state] | "checked" | "unchecked" |
[data-highlighted] | Present while focused |
[data-disabled] | Present when disabled |
A nested menu. Wraps a sub trigger and its sub content inside a parent content.
| Prop | Type | Default | Description |
|---|
open | Signal<boolean> | boolean | false | The open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state. |
The item that opens its submenu — on hover after openDelay, ArrowRight, Enter, Space, or click. Sets aria-haspopup and aria-expanded. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
openDelay | number | 100 | How long the pointer must rest on the trigger before the submenu opens, in milliseconds. |
disabled | Signal<boolean> | boolean | false | Prevents opening the submenu; the keyboard skips the item. |
| Data attribute | Value |
|---|
[data-context-menu-sub-trigger] | Present |
[data-context-menu-item] | Present |
[data-state] | "open" | "closed" |
[data-highlighted] | Present while focused |
[data-disabled] | Present when disabled |
The submenu's panel, positioned against its trigger. ArrowLeft closes it and returns focus; the keyboard model inside matches the parent content. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|
side | "top" | "bottom" | "left" | "right" | "right" | Preferred side of the sub trigger to place the panel. |
align | "start" | "center" | "end" | "start" | How the panel aligns along the chosen side. |
offset | number | 0 | Distance in pixels between the sub trigger and the panel. |
loop | boolean | true | Whether arrow keys wrap from the last item back to the first. |
| Data attribute | Value |
|---|
[data-context-menu-sub-content] | Present |
[data-state] | "open" | "closed" |
[data-side] | "top" | "bottom" | "left" | "right" |
[data-align] | "start" | "center" | "end" |
Wraps related items in role="group", labeled by the heading placed inside it. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|
[data-context-menu-group] | Present |
Names the group it sits in; the group points aria-labelledby at it. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|
[data-context-menu-group-heading] | Present |
A role="separator" line between sections. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|
[data-context-menu-separator] | Present |
The core Portal helper, for rendering the panel into another DOM parent to escape overflow and stacking.