DocsPlatform
ContextMenu
Menus opened by pointer, with typeahead and arrow-key navigation that counts by original index, so disabled rows never shift the count. Submenus use the same intent handling as the other menu components. All four layers share the one behaviour contract.
Live machine
The shipped contextMenuMachine. Right-click the pane — the machine stores the pointer position, and divider rows keep their original indexes.
context-menu · live via createService
States · walked from the machine
Transitions from here · click to send
Context
{
"mode": "uncontrolled",
"position": null,
"highlightedIndex": 0,
"typeaheadBuffer": ""
}Log · newest first
No events yet — use the component above or send a transition.
The layers
Machine is pure state with no DOM. Headless maps that state to framework-native anatomy and ARIA. Styled keeps the existing Grassroot visual recipe while delegating behaviour. Vanilla mounts the same headless contract without a framework.
Behaviour contract
The machine owns point opening, divider gaps, disabled navigation, typeahead, dismissal, and submenu intent. Adapters translate DOM events and render machine snapshots. Not one of them keeps a second reducer or re-filters the collection behind your back.
- Controlled bindings emit serialisable change requests, and only adopt the parent's state once it comes back in through input.
- Uncontrolled bindings commit straight to machine state, and still emit the same request object, so a consumer can watch either way.
- Keyboard, pointer, focus, dismissal, and accessibility behaviour are shared across vanilla, React, Vue, Solid, Svelte, and Angular.
Evidence
The authoritative source files are:
packages/ui-machines/src/context-menu/index.tspackages/ui-headless/core/src/context-menu/context-menu.spec.mdpackages/ui-headless/core/src/context-menu/vanilla.tspackages/ui-headless/browser-contract/context-menu.contract.ts, the shared browser assertions.- Each framework package has a matching
browser/context-menu.spec.tscontract entry.