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.
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
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.
<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.
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
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 (<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"andaria-expandedwhen 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 generatedaria-controlslinkage. - Separators are non-focusable and are skipped by arrow-key navigation.
Escaperestores focus to the trigger.Taband outside pointer interaction dismiss the tree without forcing a focus move.
Follows the WAI-ARIA Menu Button pattern.