Context Menu
Right-click and touch-hold context menu overlay with nested submenus, optional horizontal icon groups, shortcuts, boolean toggles, and danger actions.
import { useContextMenuAPI, contextMenu } from 'phoundry-ui'; Programmatic Open
const api = useContextMenuAPI();
const items: MenuItem[] = [
{ type: 'action', label: 'Copy', icon: 'phoundry-mono:copy', id: 'copy', action: () => {} },
{ type: 'separator' },
{ type: 'action', label: 'Delete', danger: true, id: 'del', action: () => {} },
];
api.open(() => items, e.clientX, e.clientY, { ariaLabel: 'Edit actions' });Right-Click Area (Attachment)
<div {@attach contextMenu({ api, items: menuItems })}>
Right-click here
</div>OpenMenuOptions (4th argument to api.open)
| Prop | Type | Default | Description |
|---|---|---|---|
| ariaLabel | string | - | Accessible name for the menu panel. Defaults to `"Context menu"`. |
| anchor | 'start' | 'end' | - | Horizontal alignment relative to the open point. |
| side | PopoverSide | - | Which edge of the trigger the menu opens from. Defaults to `'bottom'`. |
| onClose | () => void | - | Called when the menu closes (Escape, backdrop, or after an action). |
| trigger | unknown | - | Optional trigger reference for callers that track menu origin. |
ActionMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'action' | - | Menu item that triggers a callback. |
| id required | string | - | Stable key for shortcuts / analytics. |
| label required | string | - | Display text. |
| icon | string | - | Iconify icon string. |
| shortcut | string | - | Display-only shortcut hint. |
| pluginId | string | - | Optional plugin scope for shortcut manager lookups (`pluginId.id`). |
| danger | boolean | false | Red danger styling. |
| disabled | boolean | false | Non-interactive row. |
| selected | boolean | - | Decorates the row as checked/active where supported. |
| preventClose | boolean | false | Keep the overlay open after invoking the action. |
| action required | () => void | Promise<void> | - | Handler run when the row is activated. |
BooleanMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'boolean' | - | Toggle row with checkbox on the right. |
| label required | string | - | Display text. |
| icon | string | - | Iconify icon string. |
| pluginId | string | - | Plugin ID (same role as on action items). |
| preventClose | boolean | false | Keep the menu open after toggling. |
| value required | boolean | - | Checked state. |
| onchange required | (nextValue: boolean) => void | Promise<void> | - | Called with the new value after the user toggles the row. |
SubmenuMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'submenu' | - | Nested submenu container. |
| label required | string | - | Submenu trigger label. |
| items required | MenuItem[] | - | Child menu items. |
GroupMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'group' | - | Horizontal row of icon-only controls. |
| id required | string | - | Stable key (see MenuItemBase). |
| items required | GroupMenuChildItem[] | - | Toolbar row: **`action`** and **`boolean`** as icon-only `Button`s (labels via tooltip / `aria-label`); **`separator`** as a vertical rule. |
| disabled | boolean | false | Disables all controls in the group. |
SeparatorMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'separator' | - | Horizontal rule between groups. |
LabelMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'label' | - | Non-interactive heading row. |
| label required | string | - | Muted caption text. |
CustomMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| type required | 'custom' | - | Fully custom row body via snippet. |
| id required | string | - | Stable key. |
| render required | Snippet<[context?: CustomMenuItemContext]> | - | Snippet rendered inside the menu row. The optional context can open a descendant panel from one of the snippet controls or close the current panel. |
| disabled | boolean | false | Skip pointer interaction. |
Custom menu descendants
The snippet context exposes openSubmenu(item, trigger, intent?) and closePanel(). Use openSubmenu when an editor rendered by a CustomMenuItem needs another menu level, such as an option-level editor. Pass the control element that should anchor the child panel and use intent: 'keyboard' for keyboard activation or 'touch' for immediate touch navigation.
Descendants opened this way stay in the same menu session. Pointer opens preserve neutral focus; keyboard opens focus the first interactive control. Arrow Left or Escape closes the current child panel and returns focus to its trigger. closePanel() closes only the panel containing that custom item (or the root session when called at the root).
Touch and custom contextual callbacks
contextMenu({ api, items }) opens on a stationary single-finger hold as well as right-click. It uses the exported longPress attachment, which can also invoke custom or asynchronous contextual callbacks directly:
<script lang="ts">
import { longPress, useContextMenuAPI } from 'phoundry-ui';
const api = useContextMenuAPI();
function openOptions(event: MouseEvent, trigger: HTMLElement) {
api.open(buildItems(), event.clientX, event.clientY, { trigger });
}
</script>
<div {@attach longPress({ onLongPress: openOptions })}>Contextual target</div>onLongPress(event, trigger) receives the original touch PointerEvent and attached element. The event’s currentTarget is no longer available when the hold completes; use trigger for anchoring. The default hold duration is 500 milliseconds and movement tolerance is 10 CSS pixels, configurable through delay and movementTolerance.
Movement, scrolling, pointer cancellation, another touch, window blur, and attachment teardown cancel recognition. A recognized hold consumes its release click and native context menu. Normal taps and scrolling stay native. Inputs, textareas, selects, editable/textbox regions, selectable .select-text regions, and nested interactive controls are excluded; mark other native selection or preview regions with data-native-touch.
Tap a submenu to open it immediately. Touch does not depend on hover or pointer-leave timers. Below 520 CSS pixels, touch navigation presents the deepest panel with a Back control that returns to its parent. Panels remain within the viewport and scroll when their contents are taller than the screen. An outside click or completed touch tap dismisses the session and consumes that gesture, so the underlying content is not activated. A cancelled touch leaves the menu open; another tap or Back can still dismiss it.
Semantic Back
api.back() closes the deepest open submenu and returns focus to its trigger. With no submenu open, it closes the root session; with no menu open, it returns false. api.close() always closes the whole session. Each api.open() starts a fresh session, including when replacing an existing menu or reopening during its exit transition; live item-source updates and bumpTick() retain the current submenu branch.
For application Back handling across menus and Popovers, import getContextualDismissal from phoundry-ui. Its reactive active property reports mounted contextual owners, and back() asks the most recently mounted owner to dismiss one level, returning whether it consumed Back. Call it before dismissing the underlying application surface. Owners release registration when they close or unmount. The interface does not manage browser history, dialogs, or comboboxes.
Usage tips
provideContextMenu()must be called in the root layout, andContextMenuOverlaymust be rendered there - or callsetupOverlays()once to initialize all overlay managers together.- The menu auto-positions to stay within the viewport.
- Pass
setupOverlays({ contextMenu: { presentation: 'sheet' } })to present every context menu as a full-width, bottom-anchored sheet with one level visible at a time and a Back control. The setting applies to all menus in the session;x,y, andanchorare ignored in sheet mode. - Arrow keys, Home, and End move the highlighted row; Enter and Space activate it. Escape closes the menu. The active row uses the same background as hover; focus returns to the element that opened the menu.
- Nested overlays opened from custom menu content (a dialog popover, Select dropdown, or similar) do not dismiss the child panel while the pointer is over them.
api.openaccepts either a static array or() => MenuItem[]so menus can read live state (as with the attachment demo). PassariaLabelin the fourth argument when the default"Context menu"label is too generic.- For click-based dropdowns from a button, use
ButtonDropdowninstead.
Context menus and both Popover forms portal their roots to document.body. Opening or promoting an owner places its root after existing contextual owners so paint order follows Back ownership.