0.1.0

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.

InteractionResult
Click the triggerOpens the menu and focuses its first enabled item.
ArrowDown, Enter, or Space on the triggerOpens the menu and focuses its first enabled item.
ArrowUp on the triggerOpens the menu and focuses its last enabled item.
Hover a branchOpens its submenu after 200 ms without moving focus.
ArrowRight, Enter, or Space on a branchOpens its submenu and focuses the first enabled child.
ArrowLeft in a submenuCloses that submenu and returns focus to its branch.
EscapeCloses the complete tree and returns focus to the trigger.
Tab or an outside pointer interactionCloses 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:

PartCSS 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

AttributeTypeDefaultDescription
openbooleanfalseWhether the menu starts open.
placementtop · … · left-endbottom-startPlacement of the menu surface relative to its trigger.

Light-DOM contract

TargetRequiredHost writesAuthor owns
[data-ref="trigger"]yesaria-haspopup, aria-expanded, aria-controlstrigger button
[data-ref="menu"]yeshiddenpopup (role="menu")
[role="menuitem"]yesaria-expanded on brancheslabel, optional data-value
[data-autocomplete-input]nofocus on openfilter field inside menu

Composition

PieceDescription
triggerLabel for the menu button.
itemsOptional recursive RuiMenuEntry[] data source. A child items array makes an item a submenu branch; { type: 'separator' } divides groups.
childrenExplicit trigger and menu composition helpers.

Events

EventDetailDescription
rui-change{ value: string }Emitted when a menu item is activated; value is the item's data-value or text.
rui-closeEmitted when the menu closes.

View helpers

ComponentTarget stampedNotes
RuiMenuButton<rui-menu-button>trigger + items convenience API
RuiMenuButtonTriggerdata-ref="trigger"
RuiMenuButtonContentdata-ref="menu", role="menu"
RuiMenuButtonSubmenuContentdata-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):

ClassDescription
.rui-menu-buttonRoot wrapper around trigger and menu.
.rui-menu-button__triggerTrigger button (composes rui-button--primary).
.rui-menu-button__chevronChevron indicator.
.rui-menu-button__menuPopup menu surface (role="menu", rui-popover rui-floating).
.rui-menu-button__submenuNested popup menu surface.
.rui-menu-button__itemMenu item (role="menuitem", authored by the view helper).
.rui-separatorGeneric divider; menu surfaces add vertical spacing.

Theme roles

PartCSS 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.