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

live
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.

PropTypeDefaultDescription
optionsarray

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onopen
  • onclose

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.