Context menu
Offer commands for a specific object through a real contextual trigger and an equally reachable visible alternative.
Choose object-specific actions
ContextMenu associates commands with the object under a context action. Use the actual installed context-menu root/trigger, which provides a virtual pointer anchor and touch behavior. A normal MenuTrigger is a click button, even if documentation calls it right-click.
Keep important actions discoverable through a visible command button or direct action too. The example exposes the same bookmark action through the contextual region and an ordinary keyboard/single-pointer menu button. Native text selection/browser commands should not be suppressed across an entire page.
Anatomy and specifications
| Part | Actual contract |
|---|---|
| ContextMenu | Installed context state root, no own DOM element; compatible menu item context |
| ContextMenuTrigger | Default div, HTMLDivElement ref; contextual pointer/touch event handling. Tab focus and a visible name are caller-owned. |
| Portal / Positioner | Portal plus installed virtual pointer anchor; collision avoidance must fit the actual viewport |
| Popup / Item / Separator | Shared bounded popover surface and 44px item geometry; independent disabled semantics and task effects |
| Visible alternative | A real MenuTrigger button operating the same local action; focus returns to the surviving object context |
Working context action and visible alternative
Local reading example
Local bookmark: not included.
Right-click this region, or use the visible button below. Long press is an installed touch behavior that needs device verification.
No local context action run. The visible button offers the same local action with keyboard and single-pointer access.
'use client';
import { useRef, useState } from 'react';
import { Button, ContextMenu, ContextMenuItem, ContextMenuPopup, ContextMenuPortal, ContextMenuPositioner, ContextMenuSeparator, ContextMenuTrigger, Menu, MenuItem, MenuPopup, MenuPortal, MenuPositioner, MenuTrigger } from '@mobtranslate/ui';
export function ContextCommandsExample() {
const [bookmarked, setBookmarked] = useState(false);
const [result, setResult] = useState('No local context action run.');
const record = useRef<HTMLDivElement>(null);
function toggleBookmark() { setBookmarked(current => !current); setResult('Local bookmark toggled. No remote request occurred.'); }
return <div data-context-lab className="space-y-4">
<ContextMenu>
<ContextMenuTrigger ref={record} data-context-target role="group" tabIndex={0} aria-label="Local reading example context" className="rounded-lg border-2 border-[var(--color-border)] p-4">
<p>Local reading example</p><p data-context-bookmark>Local bookmark: {bookmarked ? 'included' : 'not included'}.</p><p>Right-click this region, or use the visible button below. Long press is an installed touch behavior that needs device verification.</p>
</ContextMenuTrigger>
<ContextMenuPortal><ContextMenuPositioner sideOffset={4}><ContextMenuPopup aria-label="Local reading context actions" finalFocus={record}>
<ContextMenuItem onClick={toggleBookmark}>{bookmarked ? 'Remove local bookmark' : 'Add local bookmark'}</ContextMenuItem>
<ContextMenuSeparator /><ContextMenuItem disabled onClick={() => setResult('Unexpected unavailable context activation.')}>Share externally (unavailable)</ContextMenuItem>
</ContextMenuPopup></ContextMenuPositioner></ContextMenuPortal>
</ContextMenu>
<Menu><MenuTrigger render={<Button variant="outline">Open local context actions</Button>} /><MenuPortal><MenuPositioner sideOffset={8}><MenuPopup aria-label="Visible local context actions" finalFocus={record}>
<MenuItem onClick={toggleBookmark}>{bookmarked ? 'Remove local bookmark' : 'Add local bookmark'}</MenuItem><MenuItem disabled>Share externally (unavailable)</MenuItem>
</MenuPopup></MenuPositioner></MenuPortal></Menu>
<p role="status">{result} The visible button offers the same local action with keyboard and single-pointer access.</p>
</div>;
}
API reference
| Export / prop | Installed contract |
|---|---|
| ContextMenu open / defaultOpen / onOpenChange | Installed context-menu state and change details; root does not expose a modal prop. |
| ContextMenuTrigger render / native props / ref | Context region element and events; ordinary clicking is not its menu activation contract. |
| ContextMenuPortal / ContextMenuPositioner | Installed portal/container/keepMounted and collision/side/offset props; anchor is context position rather than a normal button. |
| ContextMenuPopup finalFocus | Installed menu restoration policy; explicit surviving object ref in this example. |
| ContextMenuItem onClick / disabled / label / closeOnClick | Actual command; true closeOnClick default. Native/context access must produce the same effect. |
| ContextMenuSeparator | Installed presentation separator. Shared Menu choice/group/submenu parts may be composed because the installed contexts are compatible. |
| className / style / render / refs | Installed part contracts; styled wrappers retain string-or-state class callbacks. Default item/submenu refs target their actual elements. Custom renders must preserve events, roles, names and refs. |
Keyboard, touch and state
- Right-click opens at the contextual position. Long press is supplied by the installed primitive; verify threshold, scrolling/movement cancellation and platform behavior on a real touch device.
- Browser/OS context-menu keys and Shift+F10 behavior differ. Do not promise that the trigger itself implements a keyboard shortcut; use the verified visible button route as an explicit alternative.
- Closed/open state and disabled item behavior are distinct from the object’s bookmarked/retained state. The example changes actual text and keeps one local bookmark state for both routes.
- Escape or activation restores focus to the explicit surviving context. Test default browser commands and selection outside this intentionally contextual region.
Failures and recovery
- The previous wrapper aliased normal Menu.Root/Trigger and did not provide the documented contextual behavior. Migrating to the real primitive changes default Trigger from button to div and removes normal trigger-specific props; review callers and refs.
- Do not make long press, right click or an undiscoverable keyboard gesture the only path to a command. A visible alternative must run the same action.
- Avoid fake clipboard/share/inspect actions. Real clipboard or external sharing needs permission/failure recovery and observed output; this example claims only a local bookmark.
- Object-specific commands need a known target. A menu opened over an old or removed record must not silently operate on another record. Preserve request identity in async tasks.
Verification and release scope
- Verify the actual command effect, requested/retained state, recovery and focus after activation. A closed menu or click callback is not a save receipt.
- Check trigger naming/expanded state, item roles, group names, checked states, disabled enforcement, keyboard entry/exit, type navigation, submenu return and focus restoration. Inspect the exact installed behavior rather than assuming an upstream convention.
- Check both themes, readable text/focus cues, long labels, bounded scrolling, 320px reflow, reduced motion, forced colors, zoom and the actual touch device. Context long press is not proven by mouse tests.
- Use the checked example sources and the menu-family browser suite, then a named screen-reader/browser pair for spoken output. These local English-only examples prove no server write, permission, source authority or qualified review.