Context Menu
import { ContextMenu } from '@absoluteui/components/context-menu';import '@absoluteui/components/context-menu/style.css';Using Angular? See Context Menu for Angular.
What it does
Section titled “What it does”A Menu opened as a context menu: on the contextmenu event at the pointer position, or from the keyboard with Shift+F10 or the ContextMenu key at the focused element inside the target. It reuses the Menu engine (WAI-ARIA APG Menu), so submenus, checkbox and radio items, typeahead, and the overlay stack behave exactly as in Menu. The browser’s native menu stays reachable: Shift+right-click is left to the browser by default (nativeModifier: 'shift'), and disabled keeps the native menu entirely.
When to use
Section titled “When to use”- Shortcuts to commands for an object under the pointer (rows, files, canvas objects) that are also available elsewhere in the UI.
- Per-row commands in a list or table, using the origin in the select detail to find the row.
When not to use
Section titled “When not to use”- As the only way to reach an action: context menus are hard to discover and unavailable on many touch devices; always provide a visible control (for example a Menu button) too.
- Replacing the browser menu on text content where users expect copy, paste, spell check, or ‘open link in new tab’.
- Choosing a form value: use Select, Combobox, or Radio Group.
- Rich interactive content: use Popover or Dialog.
Examples
Section titled “Examples”One context menu for a list; find the row from origin
Section titled “One context menu for a list; find the row from origin”import { ContextMenu, type ContextMenuSelectDetail } from '@absoluteui/components/context-menu';
const list = document.querySelector<HTMLUListElement>('#files');if (list) { // The target gets no ARIA attributes: describe the menu so it is discoverable. const hint = document.createElement('p'); hint.id = 'files-hint'; hint.textContent = 'Right-click a file or press Shift+F10 for actions. The same actions are in each row\'s Actions button.'; list.before(hint); list.setAttribute('aria-describedby', hint.id); ContextMenu.create(list, { label: 'File actions', items: [ { id: 'rename', label: 'Rename' }, { id: 'share', label: 'Share', items: [{ id: 'copy-link', label: 'Copy link' }] }, { type: 'separator' }, { id: 'delete', label: 'Delete', tone: 'danger' }, ], }); list.addEventListener('aui-select', (event) => { const { id, origin } = (event as CustomEvent<ContextMenuSelectDetail>).detail; const row = origin.closest<HTMLLIElement>('li[data-file]'); if (row === null) return; row.dataset.lastAction = id; });}Different items per origin with update() from a capture-phase listener
Section titled “Different items per origin with update() from a capture-phase listener”import { ContextMenu, type MenuItem } from '@absoluteui/components/context-menu';import { Button } from '@absoluteui/components/button';
const canvas = document.createElement('div');canvas.tabIndex = 0;canvas.setAttribute('aria-label', 'Drawing');const shape = document.createElement('button');shape.type = 'button';shape.dataset.shape = 'circle';shape.textContent = 'Circle';Button.enhance(shape, { variant: 'outline' });canvas.append(shape);document.body.append(canvas);
const shapeItems: MenuItem[] = [ { id: 'bring-front', label: 'Bring to front' }, { id: 'delete-shape', label: 'Delete shape', tone: 'danger' },];const canvasItems: MenuItem[] = [ { id: 'paste', label: 'Paste', shortcut: 'Ctrl+V' }, { type: 'checkbox', id: 'snap', label: 'Snap to grid', checked: true },];const menu = ContextMenu.create(canvas, { items: canvasItems });
// Capture phase runs before the component's own listener, so the right items are in place when it opens.const chooseItems = (event: Event): void => { const onShape = event.target instanceof Element && event.target.closest('[data-shape]') !== null; menu.update({ items: onShape ? shapeItems : canvasItems });};canvas.addEventListener('contextmenu', chooseItems, { capture: true });canvas.addEventListener('keydown', (event) => { if (event.key === 'ContextMenu' || (event.key === 'F10' && event.shiftKey)) chooseItems(event);}, { capture: true });Global defaults
Section titled “Global defaults”import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { contextMenu: { nativeModifier: 'none', closeOnSelect: false } } });import { ContextMenu, type ContextMenuSelectDetail } from '@absoluteui/components/context-menu';import '@absoluteui/components/menu/style.css';import '@absoluteui/components/context-menu/style.css';
const table = document.querySelector<HTMLTableElement>('#orders');if (table) { const menu = ContextMenu.create(table, { label: 'Order actions', items: [ { id: 'open', label: 'Open' }, { id: 'copy-id', label: 'Copy order id', shortcut: 'Ctrl+C' }, { type: 'separator' }, { id: 'cancel', label: 'Cancel order', tone: 'danger' }, ], }); table.addEventListener('aui-select', (event) => { const { id, origin } = (event as CustomEvent<ContextMenuSelectDetail>).detail; const row = origin.closest<HTMLTableRowElement>('tr[data-order]'); if (row) row.dataset.lastAction = id; }); menu.element.addEventListener('aui-close', () => { table.dataset.menu = 'closed'; });}More usage
Section titled “More usage”import { ContextMenu, type ContextMenuSelectDetail } from '@absoluteui/components/context-menu';
const list = document.querySelector<HTMLElement>('#files');if (list) { ContextMenu.create(list, { items: [{ id: 'rename', label: 'Rename' }, { id: 'delete', label: 'Delete', tone: 'danger' }] }); list.addEventListener('aui-select', (event) => { const { id, origin } = (event as CustomEvent<ContextMenuSelectDetail>).detail; const row = origin.closest<HTMLElement>('[data-file]'); if (row) row.dataset.lastAction = id; });}To vary items per origin, call update({ items }) from a capture-phase contextmenu (and keydown) listener on the target: it runs before the component’s own listener. More examples, type-checked in CI: metadata/context-menu/examples.json.
Anatomy
Section titled “Anatomy”<div id="files"> (consumer target; no attributes added) <li data-file="a.txt">...</li> (right-clicked or focused element = origin)</div>...<div class="aui-menu aui-menu--context" role="menu" popover="manual" aria-label="Context menu"> (end of the target's <dialog>, or <body>) <div class="aui-menu__item" role="menuitem" tabindex="-1">...</div> (Menu items, roving tabindex; see Menu)</div><div class="aui-menu aui-menu--submenu" role="menu" popover="manual"> (one per submenu)API reference
Section titled “API reference”Factory
Section titled “Factory”ContextMenu.create(target: HTMLElement, options: ContextMenuOptions): ContextMenuGlobal configuration key: contextMenu (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
items |
readonly MenuItem[] |
Menu items (the Menu item model: action with optional submenu items, checkbox, radio, separator, group). Ids are unique across the whole tree. An empty array is allowed. Required. | |
label |
string |
aria-label of the menu. Default: localized ‘Context menu’. Must be non-empty when set. | |
disabled |
boolean |
false |
While true, right-click and the keyboard keys keep the browser’s native behavior and open() is still possible; setting it closes an open menu. |
renderItem |
(item: MenuSelectableItem, context: MenuRenderContext) => Node |
Custom item content (since 0.9.0); the label stays the accessible name and typeahead text. Nodes are reused per item and released with releaseNode when dropped. | |
icons |
Partial<{ submenu: IconRef; checked: IconRef; radio: IconRef }> |
Icon slot overrides. | |
closeOnSelect |
boolean |
true |
Close after activating a checkbox or radio item. Action items always close the menu. Globally configurable. |
typeahead |
boolean |
true |
Printable characters move focus to the next item whose label starts with them. Globally configurable. |
nativeModifier |
'shift' | 'none' |
"shift" |
‘shift’: Shift+right-click is left to the browser’s native menu. ‘none’: every right-click opens the component menu. Globally configurable. |
direction |
'ltr' | 'rtl' |
"ltr" |
Logical alignment, submenu side, submenu arrow keys, and chevron mirroring. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
target |
HTMLElement |
The element whose content opens the menu. It receives no ARIA attributes. |
element |
HTMLDivElement |
Top-level role=menu popup (popover=manual, class aui-menu aui-menu–context), appended to the target’s enclosing <dialog> or <body>. |
isOpen |
boolean |
Whether the menu is open. |
origin |
Element | null |
Element the open (or last opened) menu belongs to: the right-clicked element, the focused element, or the target for open(). Null before the first open and after destroy(). |
options |
Readonly<ContextMenuOptions> |
Current options. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
open(point?: { x: number; y: number }): void |
Open at viewport (client) coordinates, or below the target when no point is given; focus moves to the first enabled item and origin becomes the target. Non-finite coordinates throw CONTEXT_MENU_OPTIONS. |
close(): void |
Close all levels; focus is restored only when it was inside the menu. No aui-select. |
update(options: Partial<ContextMenuOptions>): void |
Merge, validate, and apply options; new items reset checkbox/radio state and close open submenus. A failed update keeps the previous options. |
destroy(): void |
Close, remove all popups, listeners, and subscriptions; the target keeps the native menu again. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-select |
{ id: string; item: MenuSelectableItem; checked?: boolean; origin: Element } |
Yes | No | Dispatched on the target when the user activates an enabled item. origin is the element the menu was opened for; checked is the new state for checkbox and radio items. |
aui-open |
null |
No | No | Dispatched on the menu element after the top level opened. |
aui-close |
null |
No | No | Dispatched on the menu element after all levels closed. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-menu--context |
Added to the top-level popup; sets a 12rem minimum inline size. |
aui-menu (and all aui-menu__* classes) |
Menu popup and items; styled by @absoluteui/components/menu/style.css. |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
submenu |
chevron-right |
checked |
check |
radio |
check |
TypeScript definitions
Section titled “TypeScript definitions”type ContextMenuNativeModifier = 'shift' | 'none';interface ContextMenuConfig { closeOnSelect: boolean; typeahead: boolean; nativeModifier: ContextMenuNativeModifier }interface ContextMenuOptions extends Partial<ContextMenuConfig> { items: readonly MenuItem[]; label?: string; disabled?: boolean; direction?: 'ltr' | 'rtl'; icons?: Partial<MenuIcons>;}interface ContextMenuSelectDetail extends MenuSelectDetail { origin: Element }MenuItem, MenuSelectableItem, MenuSelectDetail, and MenuIcons are re-exported from @absoluteui/components/context-menu.
Accessibility
Section titled “Accessibility”Pattern: WAI-ARIA APG Menu (the Menu engine) opened as a context menu at the pointer or at the focused element; roving focus with real DOM focus on items.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Shift+F10 / ContextMenu | Focus inside the target, not disabled | Opens the menu at the focused element and focuses the first enabled item. |
| ArrowDown / ArrowUp | Menu | Next / previous item, wrapping. Disabled items receive focus. |
| Home / End | Menu | First / last item. |
| Printable characters | Menu, typeahead=true | Focus the next item whose label starts with the typed characters. |
| Enter | Item focused | Activates the item (closes, restores focus, emits aui-select), toggles a checkbox or checks a radio, or opens a submenu. |
| Space | Item focused | Same as Enter; with closeOnSelect=false, toggles checkbox/radio items without closing. |
| ArrowRight (ArrowLeft in RTL) | Submenu parent focused | Opens the submenu and focuses its first item. |
| ArrowLeft (ArrowRight in RTL) | Submenu | Closes the submenu and returns focus to its parent item. |
| Escape | Submenu | Closes that submenu only and returns focus to its parent item. |
| Escape | Top-level menu | Closes the menu and restores focus to the previously focused element; an enclosing Dialog stays open. |
| Tab / Shift+Tab | Menu | Closes all levels, restores focus, and lets the browser move focus on from there. |
| Element | Role |
|---|---|
| target | unchanged (no role or ARIA attributes are added) |
| popup and submenus | menu |
| items | menuitem, menuitemcheckbox, or menuitemradio |
| group | group |
| separator | separator |
| icons | none (aria-hidden=true) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-label |
top-level menu | The label option, default localized ‘Context menu’ |
aria-haspopup |
submenu parent items | “menu” |
aria-expanded |
submenu parent items | “true” while the submenu is open |
aria-controls |
submenu parent items | id of the submenu |
aria-labelledby |
submenu | Label of its parent item |
aria-labelledby |
items | The item’s label text |
aria-describedby |
items | Description and shortcut hint ids |
aria-disabled |
disabled items | “true”; the item stays focusable but is not activatable |
aria-checked |
checkbox and radio items | “true” | “false” |
Opening moves real DOM focus to the first enabled item. Activation, Escape at the top level, Tab, and close() with focus inside restore focus to the element focused before opening (not the body), else to the target when it is focusable. Outside presses and focus moving elsewhere close without moving focus.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| Criterion | Level | Note |
|---|---|---|
| 1.3.1 Info and Relationships | A | |
| 1.4.3 Contrast (Minimum) | AA | |
| 1.4.11 Non-text Contrast | AA | |
| 2.1.1 Keyboard | A | |
| 2.1.2 No Keyboard Trap | A | |
| 2.4.3 Focus Order | A | |
| 2.4.7 Focus Visible | AA | |
| 4.1.2 Name, Role, Value | A |
Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- The target receives no ARIA attributes, so nothing announces the context menu: describe it (for example aria-describedby text ‘Press Shift+F10 for actions’) or show a visible hint.
- Never put actions only in a context menu; offer each one through a visible control as well (2.1.1, discoverability).
- Keyboard access uses Shift+F10 and the ContextMenu key at the focused element, so descendants must be focusable for per-row keyboard use.
- Popups are appended to the enclosing <dialog> so they are not inert inside a modal Dialog; each level is a separate overlay-stack entry.
- Right-clicking inside the open popup never shows the native menu over it.
- Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
- Inside an enabled target the native browser menu is not reachable from the keyboard (Shift+F10 and the ContextMenu key always open the component menu).
Behavior
Section titled “Behavior”States
Section titled “States”closed, open at the pointer, open at an element (keyboard or open() without a point), submenu open, disabled (native menu kept), item focused, item disabled, item checked.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| closed | right-click inside the target (not Shift with nativeModifier: 'shift', not disabled) |
open at the pointer; native menu suppressed; first enabled item focused; aui-open |
| closed | Shift+F10 or ContextMenu key with focus inside the target (not disabled) |
open below the focused element |
| closed | open(point?) |
open at the point, or below the target |
| open | right-click inside the target again | closed and reopened at the new point and origin |
| open | activate an action item | closed; focus restored; aui-close, then aui-select |
| open | activate a checkbox/radio item | state toggled; closed only when closeOnSelect is true |
| open | Escape at the top level | closed; focus restored |
| open | Tab, outside press, focus moving elsewhere, close(), update({ disabled: true }) |
closed |
Accessibility notes
Section titled “Accessibility notes”- Nothing announces that a context menu exists. Add a visible hint or a description, for example
aria-describedbytext “Press Shift+F10 for actions”, on the target or its rows. - Never put actions only in a context menu; offer each one through a visible control as well.
- For per-row keyboard use, rows (or controls in them) must be focusable: the keyboard opens the menu at the focused element.
- Focus is restored to the element focused before opening (not
<body>), else to the target when it is focusable. - Inside a modal Dialog the popup is appended to the dialog, so it is not inert; Escape closes the menu before the dialog.
- Manual screen-reader verification is pending (see Known limitations).
Responsive behavior
Section titled “Responsive behavior”The popup is at least 12rem wide (Menu maximum min(22rem, 100vw - 1rem)). Its height is capped by popover-max-height and the space available in the viewport, then it scrolls. The positioning engine flips and shifts every level to keep it on screen. At the pointer, the menu’s corner sits 1px from the pointer so the first item is not under it; the anchor moves with the target when the page scrolls.
Right-to-left
Section titled “Right-to-left”With direction: 'rtl' (option or global direction), bottom-start alignment is mirrored, submenus open to the left, ArrowLeft opens a submenu and ArrowRight closes it, and the chevron is mirrored through [data-direction='rtl']. An inherited dir="rtl" alone does not switch the menu; set direction.
Theming
Section titled “Theming”Identical to Menu: role tokens only, verified in the light, dark, and high-contrast themes; forced-colors mode draws the focused item in Highlight / HighlightText and disabled items in GrayText.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { contextMenu: { nativeModifier: 'none', closeOnSelect: false } } });Changes apply to existing instances: direction updates immediately and an open menu is repositioned; nativeModifier, closeOnSelect, and typeahead apply to the next interaction. The global menu configuration does not apply.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. All options are dynamic.
icons.submenu (default chevron-right), icons.checked (default check), and icons.radio (default check). Item icons are set per item with icon.
Security
Section titled “Security”All text uses textContent; nothing is parsed as HTML. Positioning writes styles through the CSSOM. CSP-safe. The native menu is suppressed only for right-clicks the component handles and inside its own popup.
Data handling
Section titled “Data handling”No persistence, network access, or telemetry. Checkbox and radio state is kept in memory and reset by update({ items }).
Performance
Section titled “Performance”All levels are built once per items change, detached, then appended. Two listeners on the target, one on the document, the Menu engine’s delegated listeners per level, and one configuration subscription. Budget: under 100 ms for ContextMenu.create plus open() with 200 items.
Supported data scale
Section titled “Supported data scale”Tested with 200 items in one level and three nested levels. Use one instance per container (not per row) and identify the row from origin.
Edge cases
Section titled “Edge cases”- An empty
itemsarray is allowed; the menu opens with focus on the menu container. - Duplicate ids anywhere in the tree throw
CONTEXT_MENU_OPTIONS. - Shift+right-click with
nativeModifier: 'shift'is not handled at all (nopreventDefault()), so the browser shows its native menu. - Right-clicking inside the open popup never shows the native menu.
- A keyboard-generated
contextmenuevent (no pointer position) opens at the focused element inside the target, or below the target. - When the target is
<body>the popup is inside the target; right-clicks inside the popup are ignored by the target handler. open()while open closes and reopens at the new point.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
CONTEXT_MENU_TARGET |
configuration | The target is not a connected HTMLElement (elements from another realm fail the instanceof check). |
CONTEXT_MENU_OPTIONS |
configuration | items not an array, missing or empty label or id, duplicate id, empty submenu, missing radio group, non-boolean checked; empty label; non-boolean disabled; open() with non-finite coordinates. |
CFG_INVALID_VALUE |
configuration | Invalid closeOnSelect, typeahead, nativeModifier, or direction. |
A failed construction leaves no popups, listeners, or subscriptions behind. A failed update() keeps the previous options.
Known limitations
Section titled “Known limitations”- The target gets no ARIA attributes; discoverability is the consumer’s job (hint or description).
nativeModifierapplies to pointer right-clicks only. Inside an enabled target, Shift+F10 and the ContextMenu key always open the component menu, so the native menu is not reachable from the keyboard there.- One item list per instance; vary it with
update({ items }). - The pointer anchor is fixed relative to the target’s box: scrolling content inside a scrollable target does not move the menu.
- Keyboard-generated
contextmenuevents are recognized heuristically (not button 2, at client point 0,0). - Popups are placed in the target’s enclosing
<dialog>or<body>at construction; moving the target between them needsdestroy()andcreate(). - The Menu limitations apply (150 ms submenu hover delay without a pointer-safe triangle; shortcut text is a hint only).
- Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- Opening on touch long-press, except where the browser itself fires
contextmenu. - HTML or custom nodes inside items.
- Link items (
href); navigate in theaui-selectlistener.