Skip to content

Headless Core — Context Menu

Powers Context Menu internally. Like Dropdown, it’s built on createMenuSelection for its selectItem action — the difference is where the panel opens: at the pointer position from a right-click, not anchored to a trigger element.

import { createContextMenu } from '@andersseen/headless-components/context-menu';
const ctx = createContextMenu({
closeOnSelect: true,
onOpenChange: open => console.log('Open:', open),
onPosition: pos => console.log('Position:', pos),
});
el.addEventListener('contextmenu', e => ctx.handleContextMenu(e));
document.addEventListener('keydown', e => ctx.handleKeyDown(e));
OptionTypeDefaultNotes
onOpenChange(isOpen: boolean) => void
onPosition(position: { x: number; y: number }) => voidFires every time open() runs (initial open or reposition).
closeOnSelectbooleantrueForwarded to createMenuSelection.
onSelect(e: { itemId?: string }) => voidForwarded to createMenuSelection.

isOpen, position: { x, y }.

open(position?) (re-uses the last position if you omit one), close(), selectItem(itemId?) — no queries bucket; read ctx.state.isOpen directly.

GetterReturns
getTriggerProps()data-state
getPanelProps(label?)role: 'menu', aria-label (defaults to 'Context menu'), data-state, hidden

handleContextMenu(event, triggerRect?) — call this from your element’s native contextmenu listener. It always calls event.preventDefault() (so the browser’s own right-click menu never shows), then computes the open position: event.clientX/clientY directly, or relative to triggerRect if you pass the trigger’s getBoundingClientRect() (useful for a panel positioned with position: absolute inside the trigger rather than position: fixed at the viewport level). handleKeyDown(event) closes on Escape — attach it at the document level, since a right-click doesn’t necessarily move focus onto the panel.

Right-click anywhere in the box below:

Right-click here

Dropdown — the trigger-anchored counterpart to this pointer-anchored menu.