JavaScript Top Menu
@lemonadejs/topmenu · ✓ 4 contract checks · framework-agnostic · zero dependencies
<Topmenu /> — a horizontal menu bar, composed ON the Contextmenu block
exactly like v5 (
- mousedown on a titled item toggles its dropdown (same item closes, another item switches)
- while the menu is open, hovering another top item moves the open dropdown to it (menubar behavior)
- keyboard: ArrowLeft/ArrowRight walk enabled items (wrapping, skipping disabled); with the menu open they move the OPEN dropdown; Enter or Space toggles, ArrowDown/ArrowUp open the submenu. Up/Down/Enter/ Escape inside the dropdown belong to the composed Contextmenu — closing it lands focus back on the menubar item
- focusin selects the focused item; focusout of the whole bar clears the selection highlight (the remembered index survives, as in v5)
- full ARIA: menubar / menuitem (the menubar directly owns its items), aria-haspopup, aria-expanded, aria-label, roving tabindex (one tab stop for the bar; disabled items unreachable)
v5 → v6 mapping: options keeps the v5 item model ({ title, submenu, disabled }; submenu items are Contextmenu items). self.open(index) → api.open(index). api.close() is new — the inner Contextmenu ref is private in v6, so a programmatic dismiss needs a surface.
Example
import { html } from 'lemonadejs';
import Topmenu from '@lemonadejs/topmenu';
const App = (props, { state }) => {
const action = state('nothing yet');
const pick = (name) => () => (action.value = name);
const options = [
{ title: 'File', submenu: [
{ title: 'New', icon: 'note_add', shortcut: 'Ctrl+N', onclick: pick('File > New') },
{ title: 'Open', icon: 'folder_open', shortcut: 'Ctrl+O', onclick: pick('File > Open') },
{ type: 'line' },
{ title: 'Export', icon: 'ios_share', submenu: [
{ title: 'As CSV', onclick: pick('Export > CSV') },
{ title: 'As PDF', onclick: pick('Export > PDF') },
] },
] },
{ title: 'Edit', submenu: [
{ title: 'Copy', icon: 'content_copy', shortcut: 'Ctrl+C', onclick: pick('Edit > Copy') },
{ title: 'Paste', icon: 'content_paste', shortcut: 'Ctrl+V', onclick: pick('Edit > Paste') },
{ title: 'Delete', icon: 'delete', disabled: true },
] },
{ title: 'Help', submenu: [{ title: 'About', icon: 'info', onclick: pick('Help > About') }] },
];
return html`<div>
<${Topmenu} options="${options}" />
<p>Last action: <b>${action}</b></p>
</div>`;
};Installation
npm install @lemonadejs/topmenu
import Topmenu from '@lemonadejs/topmenu';
import '@lemonadejs/topmenu/style.css';
Three deployment forms, one component:
html`<${Topmenu} />` // by value (no registration)
setComponents({ Topmenu }); // then <Topmenu /> by name anywhere
createWebComponent(Topmenu); // <lm-topmenu> 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 | — |
API
import { ref } from 'lemonadejs';
const topmenu = ref();
html`<${Topmenu} ref="${topmenu}" />`;
// topmenu.current.open(...) · topmenu.current.close(...)
open()close()
Styling
All classes follow the lm-topmenu-* 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/topmenu/contract.json';
verify.json carries the conformance proof produced by verify(Topmenu).
Looking for the v5 plugin? See the archived v5 documentation.