implement
Context Menu

Context Menu

A menu opened by right-clicking an area.

Right click here
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

ContextMenu

The root. Owns whether the menu is open and provides that to the parts inside it.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.
preventScrollbooleantrueWhen true, the page behind cannot scroll while the menu is open. The panel can still scroll if you give it overflow.

ContextMenuTrigger

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.

PropTypeDefaultDescription
disabledSignal<boolean> | booleanfalseFalls back to the native browser menu.
Data attributeValue
[data-context-menu-trigger]Present
[data-state]"open" | "closed"
[data-disabled]Present when disabled

ContextMenuContent

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.

PropTypeDefaultDescription
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.
offsetnumber0Distance in pixels between the anchor and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-context-menu-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

ContextMenuItem

One action. Sets role="menuitem"; focus follows the pointer. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
onSelect() => voidRuns when the item is activated with a click, Enter, or Space.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-item]Present
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuCheckboxItem

An item holding a checked state. Sets role="menuitemcheckbox" and aria-checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
checkedSignal<boolean> | booleanfalseThe checked state; selecting toggles it. Pass a signal to control it from outside.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-checkbox-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuRadioGroup

Wraps radio items and owns which one is checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<string | null> | string | nullnullThe checked item. Pass a signal to control it from outside.
Data attributeValue
[data-context-menu-radio-group]Present

ContextMenuRadioItem

One choice in a radio group. Sets role="menuitemradio" and aria-checked. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
value*stringIdentifies the item. Must be unique within the radio group.
closeOnSelectbooleantrueWhether selecting the item closes the menu.
disabledSignal<boolean> | booleanfalsePrevents selecting the item; the keyboard skips it.
Data attributeValue
[data-context-menu-radio-item]Present
[data-state]"checked" | "unchecked"
[data-highlighted]Present while focused
[data-disabled]Present when disabled

ContextMenuSub

A nested menu. Wraps a sub trigger and its sub content inside a parent content.

PropTypeDefaultDescription
openSignal<boolean> | booleanfalseThe open state. Pass a signal to control it from outside; a boolean seeds uncontrolled state.

ContextMenuSubTrigger

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.

PropTypeDefaultDescription
openDelaynumber100How long the pointer must rest on the trigger before the submenu opens, in milliseconds.
disabledSignal<boolean> | booleanfalsePrevents opening the submenu; the keyboard skips the item.
Data attributeValue
[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

ContextMenuSubContent

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.

PropTypeDefaultDescription
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.
offsetnumber0Distance in pixels between the sub trigger and the panel.
loopbooleantrueWhether arrow keys wrap from the last item back to the first.
Data attributeValue
[data-context-menu-sub-content]Present
[data-state]"open" | "closed"
[data-side]"top" | "bottom" | "left" | "right"
[data-align]"start" | "center" | "end"

ContextMenuGroup

Wraps related items in role="group", labeled by the heading placed inside it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-group]Present

ContextMenuGroupHeading

Names the group it sits in; the group points aria-labelledby at it. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-group-heading]Present

ContextMenuSeparator

A role="separator" line between sections. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-context-menu-separator]Present

ContextMenuPortal

The core Portal helper, for rendering the panel into another DOM parent to escape overflow and stacking.