# Menu

> Material Design 3 menus, from the HTML.

## Anatomy

A `<menu>` is the surface. Each `<li>` is an item. An icon leads its label by default; add `.suffix` to send it to the trailing edge, since a lone icon is indistinguishable from a leading one in CSS. A `<kbd>` or a `.badge` is always trailing content. An `<li class="divider" role="separator">` is a divider — `<menu>` is a list and permits only `<li>`, so a bare `<hr>` there is invalid (it still renders); a `.gap` splits groups; a `.label` is a heading. A nested `<menu>` is a flyout. The trigger’s `data-target` must match the menu’s `id`. `.menu-trigger` is the JavaScript contract.

This is the M3 Expressive *vertical menu*. Tokens follow the [M3 menu spec](https://m3.material.io/components/menus/specs). The container is `surface-container-low`, large (16dp) corners, level 2 elevation, 2dp group padding, and 112–280dp wide. Items are at least 44dp tall, with 16dp edge spacing, extra-small (4dp) corners, `label-large` / `on-surface`; the first and last item round their outer corners to 12dp. Icons are 20dp `on-surface-variant`. Selected items use medium (12dp) corners and `tertiary-container` / `on-tertiary-container`. Hover is an 8% state layer, focus and press 10%, none of which span the container. Dividers are inset.

`AutoInit()` starts every `.menu-trigger` except those marked `no-autoinit`. Menus open on click, below the trigger. Pass `coverTrigger: true` to cover the trigger. Pass `constrainWidth: false` so the menu sizes independently of the trigger.

Drop me

-   Refresh
-   Settings
-   Help
-   More

```
<button class="menu-trigger" data-target="menu1">Drop me</button>
<menu id="menu1">
  <li><a href="#!">Refresh<span class="material-symbols suffix" aria-hidden="true">refresh</span></a></li>
  <li><a href="#!">Settings<span class="material-symbols suffix" aria-hidden="true">settings</span></a></li>
  <li><a href="#!">Help<span class="material-symbols suffix" aria-hidden="true">help</span></a></li>
</menu>
```

For a menu inside a top app bar, see [Navbar menu](/navbar.html.md#navbar-menu).

## Usage and adaptive behavior

Use a menu for a temporary set of actions. Keep actions that must remain visible in a [toolbar](/toolbars.html.md). On Compact windows, consider replacing long or complex menus with a [bottom sheet](/bottom-sheet.html.md). Menus and nested flyouts work best in context on Medium and wider layouts.

The component repositions a menu when its preferred edge would be clipped. Nested menus open beside their parent item and switch sides near the viewport edge. Use dividers in scrollable menus; don’t use gaps there. Keep each row to one action, and keep icons decorative when the visible label already names the action.

When opened, focus moves to the first item. Space, Enter, Arrow Down, and Arrow Up open the menu; arrows move between items; letters use typeahead; Escape closes it and returns focus to the trigger. Disabled items remain focusable so their presence and label are discoverable, but they cannot be selected.

## Standard and grouped

**Standard** is the default: every item shares one continuous menu surface. **Grouped** separates related runs into coplanar surfaces. Add `.grouped` to the menu and place an inert `<li class="gap">` between groups. The gap is skipped by pointer and keyboard navigation.

### 1\. Standard

Open standard menu

-   Refresh
-   Settings
-   Help
-   More

### 2\. Grouped

Open grouped menu

-   Item 1
-   Item 2⌘C
-   Item 3
-   Item 4
-   Item 5

```
<!-- Standard is the default: it has no modifier class. -->
<menu id="standard-menu">
  <li><a href="#!">Refresh</a></li>
  <li><a href="#!">Settings</a></li>
</menu>

<!-- A gap starts a new grouped surface. -->
<menu id="grouped-menu" class="grouped">
  <li><a href="#!">Item 1</a></li>
  <li class="selected"><a href="#!">Item 2</a></li>
  <li class="gap" aria-hidden="true"></li>
  <li><a href="#!">Item 3</a></li>
</menu>
```

## Initialization

The IIFE bundle exposes `Expressive.Menu`. Call `init` yourself when you need options other than the defaults, or let `Expressive.AutoInit()` start every `.menu-trigger`.

```
document.addEventListener('DOMContentLoaded', function() {
  Expressive.Menu.init(document.querySelectorAll('.menu-trigger'), {
    coverTrigger: false
  });
});
```

Per-instance options can also be passed through AutoInit:

```
Expressive.AutoInit(document.body, {
  Menu: { constrainWidth: false, coverTrigger: false }
});
```

## Options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `alignment` | String | `'left'` | Edge the menu is aligned to. `'left'` or `'right'`. |
| `autoFocus` | Boolean | `true` | If true, automatically focus the menu for keyboard navigation. This option is named `autoFocus`, not `autoTrigger`. |
| `constrainWidth` | Boolean | `true` | If true, the menu is as wide as the trigger. M3 menus are independently sized — pass `false` for that. |
| `container` | Element | `null` | Element that will contain the menu. When omitted, the menu is moved next to the trigger. |
| `coverTrigger` | Boolean | `false` | If false, the menu opens below the trigger (the M3 placement). Pass `true` to cover the trigger. |
| `closeOnClick` | Boolean | `true` | If true, close the menu when a leaf item is clicked. A click on a row that owns a nested `<menu>` toggles that flyout and does not close the root. |
| `hover` | Boolean | `false` | If true, the menu opens on hover instead of click. |
| `inDuration` | Number | `150` | Enter transition duration, in milliseconds. |
| `outDuration` | Number | `250` | Exit transition duration, in milliseconds. |
| `onOpenStart` | Function | `null` | Called when the menu starts opening. |
| `onOpenEnd` | Function | `null` | Called when the menu finishes opening. A superseded transition or destruction cancels this callback. |
| `onCloseStart` | Function | `null` | Called when the menu starts closing. |
| `onCloseEnd` | Function | `null` | Called when the menu finishes closing. Reopening or destruction cancels this callback. |
| `onItemClick` | Function | `null` | Called when an item is clicked. Receives the `li`. |

### Examples

These two menus set `constrainWidth: false` so the list can be wider than the button, and use `alignment` to pick an edge.

Left Right

-   One
-   Two

-   Three
-   Four

-   One
-   Two

-   Three
-   Five

```
Expressive.Menu.init(document.querySelector('#left'), {
  alignment: 'left',
  constrainWidth: false,
  coverTrigger: false
});
```

Hover is off by default. Pass `hover: true` to open on mouse enter instead of click.

Hover me

-   One
-   Two

-   Three

```
Expressive.Menu.init(document.querySelector('#hover'), {
  hover: true,
  constrainWidth: false,
  coverTrigger: false
});
```

## Selection, shortcuts, and groups

Mark an ordinary current item with `.selected`. For a single-select or multi-select menu, give items `role="menuitemradio"` or `role="menuitemcheckbox"` and synchronize `aria-checked`. Don’t put `aria-selected` on a menu item. Trailing `<kbd>` is a keyboard shortcut. A `.gap` item splits groups — more expressive than a divider, and limited to one or two per menu. A `.label` is a 32dp group heading. Don’t put gaps in a scrollable menu.

Edit

-   Item 1 4
-   Item 2 Supporting text ⌘C
-   Item 3
-   Item 4
    
    -   Nested one
    -   Nested two
    
-   Item 5
-   Redo

```
<menu id="menu-rich" class="grouped">
  <li><a href="#!"><span class="material-symbols" aria-hidden="true">visibility</span> Item 1 <span class="badge">4</span></a></li>
  <li>
    <a href="#!">
      <span class="material-symbols" aria-hidden="true">content_copy</span>
      <span class="menu-item-text">
        <span>Item 2</span>
        <span class="supporting-text">Supporting text</span>
      </span>
      <kbd>⌘C</kbd>
    </a>
  </li>
  <li class="selected">
    <a href="#!"><span class="material-symbols" aria-hidden="true">check</span> Item 3</a>
  </li>
  <li class="gap" aria-hidden="true"></li>
  <li>
    <a href="#!"><span class="material-symbols" aria-hidden="true">person</span> Item 4</a>
    <menu>…</menu>
  </li>
  <li class="disabled" aria-disabled="true">
    <a href="#!" tabindex="-1"><span class="material-symbols" aria-hidden="true">redo</span> Redo</a>
  </li>
</menu>
```

## Color

Two mappings. **Standard** (the default) is surface-based and lower emphasis. **Vibrant** is tertiary-based and higher emphasis — add `.vibrant` and use it sparingly.

Vibrant

-   Item 1
-   Item 2
-   Item 3

```
<menu id="menu-vibrant" class="vibrant">
  <li><a href="#!"><span class="material-symbols" aria-hidden="true">visibility</span> Item 1</a></li>
  <li class="selected"><a href="#!"><span class="material-symbols" aria-hidden="true">check</span> Item 2</a></li>
</menu>
```

## Nested menus

Optional and desktop-first. Put a `<menu>` inside an `<li>` — no extra trigger, no second instance. On a fine pointer the flyout opens on hover or focus with a short scale and fade; the open flyout rounds up and the parent rounds down. On touch, a tap toggles `.open` on that row. A click on the parent row does not close the root menu.

Nested

-   One
-   More
    
    -   Nested one
    -   Nested two
    
-   Three

```
<button class="menu-trigger" data-target="menu1">Nested</button>
<menu id="menu1">
  <li><a href="#!">One</a></li>
  <li>
    <a href="#!">More</a>
    <menu>
      <li><a href="#!">Nested one</a></li>
      <li><a href="#!">Nested two</a></li>
    </menu>
  </li>
</menu>
```

## Methods

> All methods are called on the plugin instance. You can get the instance like this:

```
const instance = Expressive.Menu.getInstance(elem);
```

### .open()

Open the menu.

```
instance.open();
```

### .close()

Close the menu.

```
instance.close();
```

### .recalculateDimensions()

While the menu is open, recalculate its dimensions if its contents have changed.

```
instance.recalculateDimensions();
```

### .destroy()

Destroy the plugin instance and tear down its event handlers.

```
instance.destroy();
```

## Properties

| Name | Type | Description |
| --- | --- | --- |
| `el` | Element | The trigger the plugin was initialized with. |
| `options` | Object | The options the instance was initialized with. |
| `id` | String | ID of the menu element. |
| `menuEl` | Element | The menu element. |
| `isOpen` | Boolean | Whether the menu is open. |
| `isScrollable` | Boolean | Whether the menu content is scrollable. |
| `focusedIndex` | Number | Index of the focused item. |
