---
title: Menubar
description: Menubars provide persistent top-level menus (File, Edit, View) with keyboard traversal across items.
category: Navigation
---
import { meta as MenubarMeta, Default } from '@/content/stories/menubar';
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',
],
},
};
# Menubar
Menubars provide persistent top-level menus (File, Edit, View) with keyboard traversal across items.
## Try it
Open File, then hover Share to open its submenu without moving keyboard focus.
## Usage
Use `RuiMenubarMenu` for each top-level menu. Set `label` to name the menubar landmark.
```tsx
import {
RuiMenubar,
RuiMenubarMenu,
RuiMenubarMenuItem,
RuiMenubarSubmenuContent,
} from '@ecopages/radiant-ui/menubar';
import { RuiSeparator } from '@ecopages/radiant-ui/separator';
New
Share
Email
```
Place `RuiMenubarSubmenuContent` as the next sibling of the branch item. That sibling pair is the ARIA and positioning contract.
## Custom markup
```tsx
import '@ecopages/radiant-ui/menubar';
```
## Data-driven menus
The `items` prop supports recursively nested `items`. A top-level item with children becomes a menu trigger; a top-level item without children remains a direct menubar item. Within a popup, use `{ type: 'separator' }` in a `RuiMenuEntry[]` to divide action groups.
```tsx
import { RuiMenubar, type RuiMenubarItem } from '@ecopages/radiant-ui/menubar';
const applicationMenus: RuiMenubarItem[] = [
{
id: 'file',
label: 'File',
items: [
{ value: 'new', label: 'New' },
{ type: 'separator', id: 'sharing-actions' },
{
value: 'share',
label: 'Share',
items: [{ value: 'email', label: 'Email' }],
},
],
},
];
```
## Interaction model
The menubar keeps pointer state and keyboard focus separate. Pointer interaction opens a menu surface without moving focus into it. Keyboard interaction opens the same surface and moves focus to its first enabled item. This lets a user move the pointer through nested flyouts without an unexpected focus change.
| Interaction | Result |
| --- | --- |
| Click a top-level menu | Opens its menu; focus remains on that top-level trigger. |
| Hover a top-level menu while another is open | Switches to that menu without moving focus. A closed menubar does not open from hover. |
| `ArrowDown`, `Enter`, or `Space` on a top-level menu | Opens its menu and focuses the first 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 restores focus to its branch. |
| `ArrowLeft` or `ArrowRight` on a leaf | Closes the tree and opens the previous or next top-level menu. |
| `Escape` | Closes the complete tree and restores focus to the top-level trigger. |
| `Tab` or an outside pointer interaction | Closes the complete tree. |
When a branch opens from pointer hover, focus may remain on a previous item such as `New`. This is intentional: hover and focus convey different input modalities. Style `:hover` and `[aria-expanded="true"]` independently from `:focus-visible` so the open branch is clear without implying it has keyboard focus.
## Desktop application patterns
Menubars suit desktop-style apps. For site navigation, prefer `RuiNavigationMenu` or `RuiSidebar`.
## Theming
Menubar surfaces map to **semantic surface roles**, never Tailwind palette steps:
| Part | CSS roles |
| --- | --- |
| Bar (`rui-menubar`) | `surface`, `border`, `rounded-container` |
| Top-level item (`rui-menubar__item`) | `on-surface`; hover / expanded `--rui-menu-item-hover` (`surface-container-low`), `on-background`, `shadow-sm` |
| Popup (`rui-menubar__menu`) | `background`, `border`, `shadow-overlay`, `rounded-container` (via `rui-popover`) |
| Menu item (`rui-menubar__menu-item`) | `on-surface`; hover / expanded `--rui-menu-item-hover` (`surface-container-low`) |
Geometry shares control tokens (`--size-control-*`, `--space-control-*`, `--radius-control`) 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
`RuiMenubar` is a custom element (``) rendering a `role="menubar"` landmark. The JSX helper renders `items` as top-level items with optional popups. For explicit markup, compose the exported menu helpers rather than hand-authoring `data-ref` structure.
### Attributes
| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `''` | Accessible name for the menubar landmark. |
### Light-DOM contract
| Target | Required | Host writes | Author owns |
| --- | --- | --- | --- |
| `[data-ref="root"]` | yes | `role="menubar"` | menubar shell |
| `[data-ref="menubar-root"]` | yes | — | one top-level menu block |
| `[role="menuitem"]` | yes | `aria-expanded`, `aria-haspopup` (top-level) | label, optional `data-value` |
| `[role="menu"]` | yes | `hidden` | popup surface |
Branch pattern: `[role="menuitem"]` with `aria-haspopup` followed by sibling `[role="menu"]`.
### Events
| Event | Detail | Description |
| --- | --- | --- |
| `rui-change` | `{ value: string }` | Emitted when a menu item is activated; `value` is the item's `data-value` or text. |
### View helpers
| Component | Target stamped | Notes |
| --- | --- | --- |
| `RuiMenubar` | `` + `[data-ref="root"]` | Accepts `items` or children |
| `RuiMenubarMenu` | `[data-ref="menubar-root"]` | Top-level trigger + popup |
| `RuiMenubarMenuItem` | `[role="menuitem"]`, `data-value` | Popup action item |
| `RuiMenubarSubmenuContent` | `[role="menu"]` | Sibling of branch item |
### CSS classes
Public BEM classes on the composed light-DOM surface (documented via `@cssclass`):
| Class | Description |
| --- | --- |
| `.rui-menubar` | Menubar bar (`role="menubar"`). |
| `.rui-menubar__root` | Top-level menu root (trigger + optional popup). |
| `.rui-menubar__item` | Top-level item (`role="menuitem"`). |
| `.rui-menubar__menu` | Popup menu surface (`role="menu"`, `rui-popover rui-floating`). |
| `.rui-menubar__submenu` | Nested popup menu surface. |
| `.rui-menubar__menu-item` | Item inside a popup (`role="menuitem"`). |
| `.rui-separator` | Generic divider; popup menus add vertical spacing. |
### Theme roles
| Part | CSS variables consumed |
| --- | --- |
| Bar | `--surface`, `--border` |
| Top-level item | `--on-surface`, `--rui-menu-item-hover` (hover / expanded; defaults to `--surface-container-low`), `--on-background`, `--shadow-sm` |
| Popup | `--background`, `--border`, `--shadow-overlay` |
| Menu item | `--on-surface`, `--rui-menu-item-hover` (hover / expanded) |
| Focus ring | `--focus-ring` |
## Accessibility
- Menubar renders with `role="menubar"` and supports horizontal arrow-key traversal between top-level items.
- Popup menus use vertical arrow-key navigation; Home and End move to the first and last enabled item.
- Branches expose `aria-haspopup="menu"`, `aria-expanded`, and generated `aria-controls` linkage.
- Separators are non-focusable and are skipped by arrow-key navigation.
- Menus dismiss with Escape, Tab, outside pointer interaction, or leaf activation.
- Provide `label` so the region is announced to screen reader users.
Follows the WAI-ARIA Menubar pattern.