--- 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'; Actions Edit Delete ``` ## 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 Actions Edit Share Email Copy 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.