Context Menu
An imperative right-click context menu with pointer anchoring, grouping and submenus — built on Menu and native popover, opened with a single call
Installation
Usage
Basic Usage
Mount <ContextMenuHost /> once, then call ContextMenu.open() from any element's onContextMenu — no wrapper component, no anchor plumbing. The menu opens at the pointer; selecting an item closes it; right-clicking again while open re-anchors to the new cursor position.
Selected: -
"use client";
import { useState, type ReactNode } from "react";
import { ContextMenu, ContextMenuHost, type MenuConfig } from "@/ui";
const items: MenuConfig[] = [
{
group: "Canvas",
items: [{ label: "Copy" }, { label: "Paste" }, { label: "Duplicate" }],
},
{
group: "View",
items: [{ label: "Zoom In" }, { label: "Zoom Out" }, { label: "Reset View" }],
},
];
export default function ContextMenuBasicDemo() {
const [selected, setSelected] = useState<ReactNode>(null);
return (
<div className="flex flex-col items-center gap-3">
<ContextMenuHost />
<div
className="flex h-64 w-full max-w-md select-none items-center justify-center rounded-md border border-dashed text-sm text-muted-foreground"
onContextMenu={(e) => {
e.preventDefault();
ContextMenu.open({
x: e.clientX,
y: e.clientY,
items,
onSelect: (item) => setSelected(item.label),
});
}}
>
Right-click anywhere in this area
</div>
<p className="text-sm text-muted-foreground">Selected: {selected ?? "-"}</p>
</div>
);
}
Custom Styling
Pass classNames.content / styles.content through open() to restyle the panel. The menu items themselves (grouping, submenus, keyboard navigation) come from the Menu component — refer to its API for MenuConfig and MenuItemConfig.
Selected: -
"use client";import { useState, type ReactNode } from "react";import { ContextMenu, ContextMenuHost, type MenuConfig } from "@/ui";const items: MenuConfig[] = [ { label: "Duplicate" }, { label: "Archive" }, { group: "Danger", items: [{ label: "Delete" }] },];export default function ContextMenuCustomDemo() { const [selected, setSelected] = useState<ReactNode>(null); return ( <div className="flex flex-col items-center gap-3"> <ContextMenuHost /> <div className="flex h-64 w-full max-w-md select-none items-center justify-center rounded-md border border-dashed border-primary/50 bg-primary/5 text-sm text-muted-foreground" onContextMenu={(e) => { e.preventDefault(); ContextMenu.open({ x: e.clientX, y: e.clientY, items, onSelect: (item) => setSelected(item.label), classNames: { content: "w-56 border-primary/30" }, }); }} > Right-click to open a custom-styled menu </div> <p className="text-sm text-muted-foreground">Selected: {selected ?? "-"}</p> </div> );}API Reference
ContextMenuHost
Renders the floating menu panel whenever ContextMenu.open() is active. Mount it once near the app root — it renders nothing until a menu is opened. Closing happens automatically on outside mousedown and Escape; right-clicks inside the panel are suppressed so the browser menu never appears.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class on the anchor wrapper |
Statics
ContextMenu.open
ContextMenu.open(options: ContextMenuOpenOptions): voidOpens the menu at the given viewport coordinates.
| Option | Type | Default | Description |
|---|---|---|---|
x | number | - | Viewport x coordinate (usually e.clientX) Required |
y | number | - | Viewport y coordinate (usually e.clientY) Required |
items | MenuConfig[] | - | Menu items, flat or grouped — same shape as the Menu component Required |
onSelect | (item: MenuItemConfig) => void | - | Fired when a menu item is activated; the menu closes afterwards |
classNames | { content?: ClassNameValue } | - | Custom classes for the panel |
styles | { content?: React.CSSProperties } | - | Inline styles for the panel; anchor positioning props are managed internally |
ContextMenu.dismiss
ContextMenu.dismiss(): voidCloses the menu programmatically.
Notes
- The panel anchors to a zero-size element placed at the pointer position and flips via
positionTryFallbacksnear viewport edges — both are internal details. - Because there is no wrapper, the trigger can be any element or event source: table rows, canvas, tree nodes, or even a non-right-click gesture.