---
title: Menu Button
description: Menu buttons reveal a popup menu of actions when activated, such as overflow menus and contextual commands.
---

import { meta as MenuButtonMeta, Default } from '@/content/stories/menu-button';
import Canvas from '@/components/component-docs/canvas';
import Demo from '@/components/component-docs/demo';

export const config = {
	dependencies: {
		components: [Canvas, Demo],
		scripts: [
			'../../../components/component-docs/demo.script.tsx',
			'../../../components/component-docs/canvas.script.tsx',
			'../../../components/component-docs/controls.script.tsx',
		],
	},
};


# Menu Button

<p class="docs-lede">Menu buttons reveal a popup menu of actions when activated, such as overflow menus and contextual commands.</p>

## Try it

<Demo of={Default} meta={MenuButtonMeta} />

## Usage

Compose the trigger, menu content, and action items explicitly. Control `placement` to avoid viewport clipping.

```tsx
import {
  RuiMenuButton,
  RuiMenuButtonContent,
  RuiMenuButtonItem,
  RuiMenuButtonSubmenuContent,
  RuiMenuButtonTrigger,
} from '@ecopages/radiant-ui/menu-button';
import { RuiSeparator } from '@ecopages/radiant-ui/separator';

<RuiMenuButton placement="bottom-start">
  <RuiMenuButtonTrigger>Actions</RuiMenuButtonTrigger>
  <RuiMenuButtonContent>
    <RuiMenuButtonItem value="edit">Edit</RuiMenuButtonItem>
    <RuiSeparator />
    <RuiMenuButtonItem value="delete">Delete</RuiMenuButtonItem>
  </RuiMenuButtonContent>
</RuiMenuButton>
```

## Custom markup

```tsx
import '@ecopages/radiant-ui/menu-button';

<rui-menu-button>
  <button data-ref="trigger">Actions</button>
  <div data-ref="menu" role="menu" hidden>
    <button role="menuitem" data-value="edit">Edit</button>
  </div>
</rui-menu-button>
```

## Submenus

Place `RuiMenuButtonSubmenuContent` as the next sibling of the branch item. That sibling pair is the ARIA and positioning contract.

```tsx
<RuiMenuButton>
  <RuiMenuButtonTrigger>Actions</RuiMenuButtonTrigger>
  <RuiMenuButtonContent>
    <RuiMenuButtonItem value="edit">Edit</RuiMenuButtonItem>
    <RuiMenuButtonItem value="share" hasSubmenu>Share</RuiMenuButtonItem>
    <RuiMenuButtonSubmenuContent>
      <RuiMenuButtonItem value="email">Email</RuiMenuButtonItem>
      <RuiMenuButtonItem value="copy-link">Copy link</RuiMenuButtonItem>
    </RuiMenuButtonSubmenuContent>
  </RuiMenuButtonContent>
</RuiMenuButton>
```

The `items` prop provides the same structure recursively. Add `items` to a `RuiMenuItem` to make that item a branch; items without children are actions. Add `{ type: 'separator' }` to a `RuiMenuEntry[]` to divide action groups.

```tsx
import { RuiMenuButton, type RuiMenuEntry } from '@ecopages/radiant-ui/menu-button';

const actions: RuiMenuEntry[] = [
  { value: 'edit', label: 'Edit' },
  { type: 'separator', id: 'sharing-actions' },
  {
    value: 'share',
    label: 'Share',
    items: [
      { value: 'email', label: 'Email' },
      { value: 'copy-link', label: 'Copy link' },
    ],
  },
];

<RuiMenuButton trigger="Actions" items={actions} />
```

## Interaction model

Branch items open after a 200 ms pointer-hover delay. Moving from a branch into its flyout keeps that flyout open; hovering another branch closes the prior branch and starts the new delay. A branch does not emit `rui-change`; only a leaf action emits it, once, and closes the whole menu tree.

| Interaction | Result |
| --- | --- |
| Click the trigger | Opens the menu and focuses its first enabled item. |
| `ArrowDown`, `Enter`, or `Space` on the trigger | Opens the menu and focuses its first enabled item. |
| `ArrowUp` on the trigger | Opens the menu and focuses its last enabled item. |
| Hover a branch | Opens its submenu after 200 ms without moving focus. |
| `ArrowRight`, `Enter`, or `Space` on a branch | Opens its submenu and focuses the first enabled child. |
| `ArrowLeft` in a submenu | Closes that submenu and returns focus to its branch. |
| `Escape` | Closes the complete tree and returns focus to the trigger. |
| `Tab` or an outside pointer interaction | Closes the complete tree. |

Pointer hover and keyboard focus are separate states. A hovered branch can open while focus remains on the previously focused menu item; style `:hover` and `[aria-expanded="true"]` independently from `:focus-visible`.

## Placement

<p>Use `bottom-start` for left-aligned triggers. Flip to `top-*` when the button sits near the viewport bottom.</p>

## Theming

Menu Button surfaces map to **semantic surface roles**, never Tailwind palette steps:

| Part | CSS roles |
| --- | --- |
| Trigger (`rui-menu-button__trigger`) | `primary`, `on-primary` (via `rui-button--primary`), `--focus-ring` |
| Menu surface (`rui-menu-button__menu`) | `background`, `border`, `shadow-overlay`, `rounded-container` (via `rui-popover`) |
| Menu item (`rui-menu-button__item`) | `on-background`; hover / expanded `--rui-menu-item-hover` (`surface-container-low`); `--space-control-*` |

The trigger shares control geometry (`--size-control-md`, `--space-control-x`, `--radius-button`) with `RuiButton`. Override `--rui-menu-item-hover` on the host or an ancestor to restyle hover; map other roles at the theme layer, not in component CSS.

## API

`RuiMenuButton` is a custom element (`<rui-menu-button>`) with a composed light-DOM surface. Use the view helpers to compose the trigger and popup items, or use the convenience `trigger` / recursive `items` props for generated content.

### Attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `false` | Whether the menu starts open. |
| `placement` | `top` · … · `left-end` | `bottom-start` | Placement of the menu surface relative to its trigger. |

### Light-DOM contract

| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-ref="trigger"]` | yes | `aria-haspopup`, `aria-expanded`, `aria-controls` | trigger button |
| `[data-ref="menu"]` | yes | `hidden` | popup (`role="menu"`) |
| `[role="menuitem"]` | yes | `aria-expanded` on branches | label, optional `data-value` |
| `[data-autocomplete-input]` | no | focus on open | filter field inside menu |

### Composition

| Piece | Description |
| --- | --- |
| `trigger` | Label for the menu button. |
| `items` | Optional recursive `RuiMenuEntry[]` data source. A child `items` array makes an item a submenu branch; `{ type: 'separator' }` divides groups. |
| `children` | Explicit trigger and menu composition helpers. |

### Events

| Event | Detail | Description |
| --- | --- | --- |
| `rui-change` | `{ value: string }` | Emitted when a menu item is activated; `value` is the item's `data-value` or text. |
| `rui-close` | | Emitted when the menu closes. |

### View helpers

| Component | Target stamped | Notes |
| --- | --- | --- |
| `RuiMenuButton` | `<rui-menu-button>` | `trigger` + `items` convenience API |
| `RuiMenuButtonTrigger` | `data-ref="trigger"` | |
| `RuiMenuButtonContent` | `data-ref="menu"`, `role="menu"` | |
| `RuiMenuButtonSubmenuContent` | `data-ref="submenu-menu"`, `role="menu"` | Sibling of branch item |
| `RuiMenuButtonItem` | `[role="menuitem"]`, `data-value` | |

### CSS classes

Public BEM classes on the composed light-DOM surface (documented via `@cssclass`):

| Class | Description |
| --- | --- |
| `.rui-menu-button` | Root wrapper around trigger and menu. |
| `.rui-menu-button__trigger` | Trigger button (composes `rui-button--primary`). |
| `.rui-menu-button__chevron` | Chevron indicator. |
| `.rui-menu-button__menu` | Popup menu surface (`role="menu"`, `rui-popover rui-floating`). |
| `.rui-menu-button__submenu` | Nested popup menu surface. |
| `.rui-menu-button__item` | Menu item (`role="menuitem"`, authored by the view helper). |
| `.rui-separator` | Generic divider; menu surfaces add vertical spacing. |

### Theme roles

| Part | CSS variables consumed |
| --- | --- |
| Trigger | `--primary`, `--on-primary`, `--focus-ring` |
| Menu surface | `--background`, `--border`, `--shadow-overlay` |
| Menu item | `--on-background`, `--rui-menu-item-hover` (hover / expanded; defaults to `--surface-container-low`) |
| Geometry | `--size-control-*`, `--space-control-*`, `--radius-button` |

## Accessibility

- The trigger exposes `aria-haspopup="menu"` and `aria-expanded` when open.
- Menu items use vertical arrow-key navigation; Home and End move to the first and last enabled item.
- Branch items expose `aria-haspopup="menu"`, `aria-expanded`, and generated `aria-controls` linkage.
- Separators are non-focusable and are skipped by arrow-key navigation.
- `Escape` restores focus to the trigger. `Tab` and outside pointer interaction dismiss the tree without forcing a focus move.

Follows the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/" data-wai-aria target="_blank" rel="noopener noreferrer">WAI-ARIA Menu Button</a> pattern.
