JavaScript Context Menu
@lemonadejs/contextmenu · ✓ 6 contract checks · framework-agnostic · zero dependencies
<Contextmenu /> — built ON the Modal primitive, exactly like v5:
every menu level is a headerless, auto-adjusting Modal. Submenus flip
horizontally when out of space (inheriting the parent’s direction),
correct vertical overflow, open on a 200ms hover delay — and the full
v5 keyboard system: ArrowUp/Down cursor skipping disabled items and
separators with wrap-around, Home/End jump to the first/last enabled
item, ArrowRight into a submenu (cursor on its first enabled item),
ArrowLeft back out, Enter/Space activates, Escape closes everything —
keyboard closes hand focus back to the invoker (WCAG 2.4.3), and
aria-activedescendant on the focused wrapper tracks the cursor.
v5 → v6 mapping: open(options, x, y) and openAt(x, y | event) keep their signatures; the per-item render() DOM hook was dropped.
Example
import { html, ref } from 'lemonadejs';
import Contextmenu from '@lemonadejs/contextmenu';
const FILES = ['budget-2026.xlsx', 'launch-plan.docx', 'logo-final.svg'];
const App = (props, { state }) => {
const menu = ref();
const last = state('');
let target = '';
const options = [
{ title: 'Open', shortcut: 'Enter', onclick: () => (last.value = 'Opened ' + target) },
{ title: 'Rename', shortcut: 'F2', onclick: () => (last.value = 'Renamed ' + target) },
{ type: 'line' },
{ title: 'Delete', shortcut: 'Del', onclick: () => (last.value = 'Deleted ' + target) },
];
return html`<div>
<p style="margin:0 0 6px">Right-click a file:</p>
${FILES.map((name) => html`<div oncontextmenu="${(e) => { target = name; menu.current.openAt(e); }}"
style="padding:8px 12px;border:1px solid #ccc;margin-bottom:4px;cursor:context-menu">${name}</div>`)}
<p>${last}</p>
<${Contextmenu} ref="${menu}" options="${options}" />
</div>`;
};Installation
npm install @lemonadejs/contextmenu
import Contextmenu from '@lemonadejs/contextmenu';
import '@lemonadejs/contextmenu/style.css';
The icons come from Google Material Symbols. Load the font once per page:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
Three deployment forms, one component:
html`<${Contextmenu} />` // by value (no registration)
setComponents({ Contextmenu }); // then <Contextmenu /> by name anywhere
createWebComponent(Contextmenu); // <lm-contextmenu> in plain HTML/any framework
Props
Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.
| Prop | Type | Default | Description |
|---|---|---|---|
options | array | — |
Events
All event names are lowercase (the platform convention — LJS-305 warns otherwise).
onopenonclose
API
import { ref } from 'lemonadejs';
const contextmenu = ref();
html`<${Contextmenu} ref="${contextmenu}" />`;
// contextmenu.current.open(...) · contextmenu.current.openAt(...) · contextmenu.current.close(...)
open()openAt()close()
Styling
All classes follow the lm-contextmenu-* convention; visual variants are data-*
attributes on the root. Override freely — there is no styling engine to fight.
Contract
The machine-readable schema ships with the package:
import contract from '@lemonadejs/contextmenu/contract.json';
verify.json carries the conformance proof produced by verify(Contextmenu).
Looking for the v5 plugin? See the archived v5 documentation.