---
title: Menu Button
description: Menu buttons reveal a popup menu of actions when activated, such as overflow menus and contextual commands.
category: Navigation
---
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
Menu buttons reveal a popup menu of actions when activated, such as overflow menus and contextual commands.
## Try it
## 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';
ActionsEditDelete
```
## Custom markup
```tsx
import '@ecopages/radiant-ui/menu-button';
```
## Submenus
Place `RuiMenuButtonSubmenuContent` as the next sibling of the branch item. That sibling pair is the ARIA and positioning contract.
```tsx
ActionsEditShareEmailCopy link
```
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' },
],
},
];
```
## 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
Use `bottom-start` for left-aligned triggers. Flip to `top-*` when the button sits near the viewport bottom.
## 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 (``) 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` | `` | `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 WAI-ARIA Menu Button pattern.