---
title: "JavaScript Top Menu"
description: "The LemonadeJS Top Menu block for JavaScript: Menu bar with nested submenus. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/topmenu/
---

<link rel="stylesheet" href="/v6/contextmenu.css">
<link rel="stylesheet" href="/v6/modal.css">
<link rel="stylesheet" href="/v6/topmenu.css">

# 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 (<Contextmenu :ref="self.menu" /> inside the topmenu
template): each top item with a submenu opens one shared Contextmenu
right under itself. Ported faithfully from the v5 plugin:

  - 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

<!--example-->

```js
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

```bash
npm install @lemonadejs/topmenu
```

```js
import Topmenu from '@lemonadejs/topmenu';
import '@lemonadejs/topmenu/style.css';
```

Three deployment forms, one component:

```js
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

```js
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:

```js
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](/docs/v5/plugins/).