Skip to content

Menu Bar

NavigationSince 0.3.0
import { MenuBar } from '@absoluteui/components/menu-bar';
import '@absoluteui/components/menu-bar/style.css';

Using Angular? See Menu Bar for Angular.

A horizontal application menubar following the WAI-ARIA APG Menubar pattern. The bar is a role="menubar" row of role="menuitem" buttons with a single tab stop (roving tabindex). Items with items open a Menu, which is the same component and engine as Menu, so submenus, checkbox and radio items, typeahead, and the overlay stack behave identically. Items without items are commands: they open nothing and emit aui-select. While a menu is open, ArrowLeft/ArrowRight (mirrored in RTL) and pointer hover move to the neighbouring menu.

  • A persistent application command bar in the style of desktop software (File, Edit, View, Help) in editors and tools.
  • Several groups of commands that each need a menu, where a single Menu button would be too deep.
  • Site or app navigation made of links: use navigation links or Tabs, not role=menubar.
  • A single overflow or row-actions menu: use Menu.
  • A row of always-visible buttons without menus: use a toolbar or Button Group.
  • Vertical navigation (side menus): the menu bar is horizontal only.

Editor menubar with menus, a submenu, and a command item

Section titled “Editor menubar with menus, a submenu, and a command item”
import { MenuBar, type MenuBarSelectDetail } from '@absoluteui/components/menu-bar';
const host = document.createElement('div');
document.body.append(host);
MenuBar.create(host, {
label: 'Editor',
items: [
{ id: 'file', label: 'File', items: [
{ id: 'new', label: 'New', shortcut: 'Ctrl+N' },
{ id: 'open-recent', label: 'Open recent', items: [{ id: 'recent-report', label: 'report.md' }] },
{ type: 'separator' },
{ id: 'close-file', label: 'Close', tone: 'danger' },
] },
{ id: 'edit', label: 'Edit', items: [
{ id: 'undo', label: 'Undo', shortcut: 'Ctrl+Z' },
{ id: 'redo', label: 'Redo', shortcut: 'Ctrl+Y' },
] },
{ id: 'help', label: 'Help' },
],
});
// Command items and menu items both bubble aui-select to the host.
host.addEventListener('aui-select', (event) => {
const { id } = (event as CustomEvent<MenuBarSelectDetail>).detail;
host.dataset.lastCommand = id;
});

Checkbox and radio items that keep the menu open

Section titled “Checkbox and radio items that keep the menu open”
import { MenuBar, type MenuBarSelectDetail } from '@absoluteui/components/menu-bar';
const host = document.querySelector<HTMLElement>('#app-menu');
if (host) {
const bar = MenuBar.create(host, {
label: 'Application',
closeOnSelect: false,
items: [
{ id: 'view', label: 'View', items: [
{ type: 'checkbox', id: 'grid', label: 'Show grid', checked: true },
{ type: 'group', label: 'Zoom', items: [
{ type: 'radio', id: 'zoom-100', label: '100%', group: 'zoom', checked: true },
{ type: 'radio', id: 'zoom-200', label: '200%', group: 'zoom', checked: false },
] },
] },
{ id: 'tools', label: 'Tools', disabled: true, items: [{ id: 'macros', label: 'Macros' }] },
],
});
host.addEventListener('aui-select', (event) => {
const { id, checked } = (event as CustomEvent<MenuBarSelectDetail>).detail;
if (checked !== undefined) host.dataset[id] = String(checked);
});
bar.menu('view')?.element.addEventListener('aui-close', () => {
host.dataset.viewMenu = 'closed';
});
}
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { menuBar: { closeOnSelect: false, typeahead: false } } });
import { MenuBar, type MenuBarSelectDetail } from '@absoluteui/components/menu-bar';
import '@absoluteui/components/menu/style.css';
import '@absoluteui/components/menu-bar/style.css';
const host = document.querySelector<HTMLElement>('#app-menu');
if (host) {
const bar = MenuBar.create(host, {
label: 'Editor',
items: [
{ id: 'file', label: 'File', items: [
{ id: 'new', label: 'New', shortcut: 'Ctrl+N' },
{ id: 'open-recent', label: 'Open recent', items: [{ id: 'recent-report', label: 'report.md' }] },
] },
{ id: 'view', label: 'View', items: [{ type: 'checkbox', id: 'grid', label: 'Show grid', checked: true }] },
{ id: 'help', label: 'Help' },
],
});
host.addEventListener('aui-select', (event) => {
const { id, checked } = (event as CustomEvent<MenuBarSelectDetail>).detail;
if (id === 'help') bar.close();
if (checked !== undefined) host.dataset[id] = String(checked);
});
}
import { MenuBar } from '@absoluteui/components/menu-bar';
const host = document.querySelector<HTMLElement>('#app-menu');
if (host) {
MenuBar.create(host, {
label: 'Editor',
items: [
{ id: 'file', label: 'File', items: [{ id: 'new', label: 'New' }, { id: 'save', label: 'Save' }] },
{ id: 'edit', label: 'Edit', items: [{ id: 'undo', label: 'Undo' }] },
{ id: 'help', label: 'Help' },
],
});
}
<div class="aui-menu-bar"> (consumer host; children replaced)
<div class="aui-menu-bar__bar" role="menubar" aria-label="Editor">
<div class="aui-menu-bar__entry" role="none"> (display: contents)
<button class="aui-menu-bar__item" role="menuitem" tabindex="0" data-id="file"
aria-haspopup="menu" aria-expanded="false" aria-controls=menu>
<svg class="aui-menu-bar__icon"> (optional)
<span class="aui-menu-bar__label">File</span>
</button>
<div class="aui-menu" role="menu" popover="manual" aria-labelledby=button> (Menu popup, top layer)
<div class="aui-menu aui-menu--submenu" role="menu"> (one per submenu)
</div>
<div class="aui-menu-bar__entry" role="none">
<button class="aui-menu-bar__item" role="menuitem" tabindex="-1" data-id="help">Help</button> (command)
</div>
</div>
</div>
MenuBar.create(host: HTMLElement, options: MenuBarOptions): MenuBar

Global configuration key: menuBar (see Configuration).

Option Type Default Description
label string Accessible name of the menubar (aria-label). Must be a non-empty string. Required.
items readonly MenuBarItem[] Non-empty list of menubar items { id, label, icon?, disabled?, items? }. Items with items open a Menu; items without are commands. Ids are unique across the bar and all of its menus. Required.
renderItem (item: MenuSelectableItem, context: MenuRenderContext) => Node Custom item content (since 0.9.0); the label stays the accessible name and typeahead text. Nodes are reused per item and released with releaseNode when dropped.
icons Partial<{ submenu: IconRef; checked: IconRef; radio: IconRef }> Icon slot overrides passed to every menu.
closeOnSelect boolean true Menus close after activating a checkbox or radio item. Action items always close the menu. Globally configurable.
typeahead boolean true Printable characters move focus to the next bar item (skipping disabled items) or menu item whose label starts with them. Globally configurable.
direction 'ltr' | 'rtl' "ltr" Arrow-key mirroring and menu/submenu sides. An inherited dir=rtl on the host also makes the bar RTL. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement The host element (class aui-menu-bar); its children are replaced.
element HTMLDivElement The role=menubar element inside the host.
options Readonly<MenuBarOptions> Current options.
Method Description
menu(id: string): Menu | null The Menu opened by a menubar item, or null for commands, disabled items, and unknown ids.
close(): void Close any open menu; focus returns to its menubar item when it was inside the menu.
update(options: Partial<MenuBarOptions>): void Merge, validate, and re-render. Focus and the tab stop stay on the same item id; open menus close and menu state resets. A failed update keeps the previous options.
destroy(): void Destroy every menu, remove listeners and subscriptions, empty the host, and remove the aui-menu-bar class.
Event Detail Bubbles Cancelable Description
aui-select { id: string; item: MenuBarItem | MenuSelectableItem; checked?: boolean } Yes No A command bar item or an enabled menu item was activated. Always dispatched on the host (menu selections are re-dispatched there). checked is the new state for checkbox and radio items.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-menu-bar Host element.
aui-menu-bar--rtl Set on the host when the effective direction is RTL.
aui-menu-bar__bar The role=menubar row (wrapping flex).
aui-menu-bar__entry role=none wrapper (display: contents) around each item and its menu popup.
aui-menu-bar__item Bar item button (role=menuitem); [aria-expanded=‘true’] while its menu is open.
aui-menu-bar__icon | __label Item parts.
aui-menu (and all aui-menu__* classes) Menu popups opened from the bar; styled by @absoluteui/components/menu/style.css.
Slot Default icon
submenu chevron-right
checked check
radio check

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-focus-ring | --aui-color-hover | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-control-height-sm | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-3

interface MenuBarItem { id: string; label: string; icon?: IconRef; disabled?: boolean; items?: readonly MenuItem[] }
interface MenuBarConfig { closeOnSelect: boolean; typeahead: boolean }
interface MenuBarOptions extends Partial<MenuBarConfig> {
label: string;
items: readonly MenuBarItem[];
direction?: 'ltr' | 'rtl';
icons?: Partial<MenuIcons>;
}
type MenuBarSelectDetail = MenuSelectDetail; // { id: string; item: MenuSelectableItem; checked?: boolean }

MenuItem, MenuSelectableItem, MenuSelectDetail, and MenuIcons are re-exported from @absoluteui/components/menu-bar. For a command bar item, detail.item is the MenuBarItem object.

Pattern: WAI-ARIA APG Menubar with Menu popups (the Menu component and engine); roving tabindex on the bar, real DOM focus on menu items.

Key Context Action
Tab / Shift+Tab Page Moves focus to or from the bar; the bar is one tab stop (the last focused item, else the first enabled item).
ArrowRight / ArrowLeft (mirrored in RTL) Bar item focused Next / previous bar item, wrapping. Disabled items receive focus.
Home / End Bar item focused First / last bar item.
Printable characters Bar item focused, typeahead=true Focus the next enabled bar item whose label starts with the typed characters.
ArrowDown / Enter / Space Bar item with a menu Opens its menu and focuses the first enabled item.
ArrowUp Bar item with a menu Opens its menu and focuses the last enabled item.
Enter / Space Command bar item (no items) Emits aui-select; focus stays on the item.
ArrowDown / ArrowUp / Home / End / typeahead Menu open Menu navigation (same as Menu).
ArrowRight (ArrowLeft in RTL) Menu open, submenu parent focused Opens the submenu (same as Menu).
ArrowLeft (ArrowRight in RTL) Submenu open Closes the submenu and returns to its parent item (same as Menu).
ArrowRight / ArrowLeft (mirrored in RTL) Menu open, key not handled by the menu Closes the menu, moves to the neighbouring bar item (wrapping), and opens its menu with focus on its first item; command items only receive focus.
Enter / Space Menu item focused Activates it (same as Menu); action items close the menu and return focus to the bar item.
Escape Top-level menu open Closes the menu and returns focus to its bar item. Submenus close one level at a time.
Tab / Shift+Tab Menu open Closes all levels, returns focus to the bar item, and moves on out of the bar.
Element Role
bar menubar
item wrappers none
bar items menuitem (native <button>)
menu popups and submenus menu
menu items menuitem, menuitemcheckbox, or menuitemradio
icons none (aria-hidden=true)
Attribute Element Value
aria-label bar The label option
aria-haspopup bar items with items “menu” (also on disabled items with items)
aria-expanded bar items with a menu “true” while the menu is open, otherwise “false”
aria-controls bar items with a menu id of the menu popup
aria-labelledby menu popup id of its bar item
aria-disabled disabled bar items “true”; the item stays focusable but never opens or emits
tabindex bar items 0 on the current item (roving tab stop), -1 on the others

The bar has one tab stop (roving tabindex follows focus). Opening a menu moves real DOM focus to its first (or last) enabled item; activation, Escape at the top level, Tab, and close() with focus inside return focus to the bar item. Bar items show color-hover while hovered or expanded and a color-focus-ring outline on :focus-visible.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.1.2 No Keyboard Trap A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Disabled items stay focusable, per the APG, so screen-reader users discover them.
  • Hovering another bar item while a menu is open switches menus (pointer movement only; touch never switches).
  • Each menu level is a separate overlay-stack entry, so Escape and outside presses close the topmost level first, including inside a Dialog.
  • aui-select ids never collide: ids are validated as unique across the bar and all menus.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
  • A global configuration change or update() while a menu is open closes it without restoring focus to the bar item.

idle, bar item focused (:focus-visible adds the ring), menu open (bar item aria-expanded="true" keeps the hover background), submenu open, bar item disabled.

From Trigger To
idle Tab into the bar the tab-stop item focused
item focused ArrowLeft/ArrowRight, Home/End, typeahead another bar item focused; tab stop moves with it
item with a menu focused click, Enter, Space, ArrowDown / ArrowUp menu open; first / last enabled item focused; aui-open on the menu
command item focused click, Enter, Space aui-select; focus stays
menu open unhandled ArrowLeft/ArrowRight menu closed; neighbouring bar item focused and its menu opened (commands are only focused)
menu open pointer moves over another bar item same as above
menu open activate an action item menu closed; focus on the bar item; aui-select
menu open Escape at the top level menu closed; focus on the bar item
menu open Tab, outside press, focus moving elsewhere, close() menu closed
any update({ items }) or update({ icons }) rebuilt; open menus closed
any other update(), global menuBar configuration change applied in place; an open menu stays open
  • One tab stop keeps the bar cheap to pass for keyboard users; the tab stop remembers the last focused item.
  • Disabled items stay focusable (APG) so users can discover them.
  • Hover switching follows real pointer movement only; touch never switches menus.
  • Focus returns to the bar item on activation, Escape, and Tab from a menu.
  • update() or a global configuration change while a menu is open closes it without restoring focus (see Known limitations).
  • Manual screen-reader verification is pending (see Known limitations).

The bar is a wrapping flex row: on narrow widths items wrap onto further lines and labels wrap anywhere. There is no overflow (“more”) collapse. Menus size and position like Menu popups and flip and shift to stay in the viewport.

The effective direction is RTL when direction (or the global direction) is 'rtl' or the host inherits dir="rtl". Then ArrowLeft moves to the next bar item, menu switching is mirrored, menus open bottom-start aligned to the item’s right edge, submenus open to the left, and the host gets aui-menu-bar--rtl. Layout uses logical properties.

Colors come from role tokens only. In forced-colors mode the bar has a CanvasText border, the expanded item is drawn in Highlight / HighlightText, and disabled items in GrayText. Menus follow the Menu theme.

configureAbsoluteUI({ components: { menuBar: { closeOnSelect: false, typeahead: false } } });

A change applies to existing bars in place (open menus stay open). The global menu configuration does not apply to menubar menus: the bar passes placement: 'bottom-start', closeOnSelect, typeahead, and direction explicitly.

Instance options override global and theme defaults. All options are dynamic through update(); only items and icons rebuild the bar.

icons.submenu (default chevron-right), icons.checked (default check), and icons.radio (default check), passed to every menu. Bar items and menu items take a per-item icon.

All text uses textContent; nothing is parsed as HTML. Positioning writes styles through the CSSOM. CSP-safe.

No persistence, network access, or telemetry. Menu checkbox and radio state is kept in memory and reset when the bar is rebuilt (update({ items }) or update({ icons })).

All menus are built on create and when items or icons change. Four delegated listeners on the bar, the listeners of each Menu, and one configuration subscription. Budget: under 50 ms for MenuBar.create with 8 menus of 20 items.

Tested with 8 bar items of 20 menu items each and submenus two levels deep. Keep the bar short enough to fit on one line at common widths.

  • Ids must be unique across the bar and all menus; a bar item and a menu item with the same id throw MENU_BAR_OPTIONS.
  • A bar item with items: [] throws; omit items for a command.
  • A disabled item with items gets no Menu: menu(id) returns null, and it cannot be opened.
  • ArrowRight on a leaf item inside a submenu moves to the next bar item (the menu does not handle it).
  • Moving to a command item while a menu is open closes the menu and focuses the command; nothing opens.
  • update({ items }) keeps focus and the tab stop on the same id when it still exists; otherwise the tab stop moves to the first enabled item.
Code Category Cause
MENU_BAR_OPTIONS configuration Empty or non-string label; items not a non-empty array; an item that is not an object; missing or empty id or label; duplicate id across the bar and menus; empty items on a bar item or submenu; invalid menu items (as in Menu).
CFG_INVALID_VALUE configuration Invalid closeOnSelect, typeahead, or direction.

A failed construction leaves the host untouched: no rendering, class, listeners, or subscriptions. A failed update() keeps the previous options.

  • Horizontal only; no overflow collapse (items wrap).
  • update({ items }) and update({ icons }) rebuild every menu: an open menu closes, focus inside it is not restored, and checkbox/radio state resets.
  • An explicit direction: 'ltr' does not override an inherited dir="rtl". Menus read the direction at render time, so a later dir change reaches them only on the next render.
  • Menu placement is fixed at bottom-start.
  • Submenus inherit the Menu limitations (150 ms hover delay without a pointer-safe triangle; shortcut text is a hint only).
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • Vertical menubars.
  • Link items (href); navigate in the aui-select listener.
  • HTML or custom nodes inside items.
  • Checkbox or radio items directly on the bar.