Menubar
A persistent bar of application commands with the full WAI-ARIA APG menubar keyboard contract: roving tabindex, Left/Right between items, Down/Enter into a submenu, Escape unwinding one level at a time, type-ahead. Submenus — nested ones included — run on the same oge-menu-list and Escape stack as every other menu in the suite.
Before reaching for it, note the APG's own caveat: a menu bar is rarely the right pattern for plain site navigation. A <nav> of links — optionally with the disclosure pattern for expandable groups — needs none of this keyboard machinery and serves link semantics better. role="menubar" is for application-style command menus: think editor File/Edit bars, not page headers. (Angular Material has no menubar at all — MatMenu is a button-triggered dropdown; the CDK's cdkMenuBar is the closest reference.)
Getting started
One items tree; children at any depth open as nested submenus. itemClick reports the item, its key and the hierarchical index path. shortcut renders a right-aligned accelerator hint (announced via aria-keyshortcuts; the binding stays yours) and badge a counter pill. Try the keyboard: Left/Right, Down to open, ArrowRight on Share, Escape to unwind.
Last click: —
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar } from '@oge-ui/navigation';
import type { OgeMenubarItemData, OgeMenubarItemClickEvent } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- role="menubar" with the full APG keyboard contract: roving
tabindex, Left/Right between items, Down/Enter opens, Escape unwinds,
type-ahead. Submenus run on the same oge-menu-list every other menu in
the suite uses. -->
<oge-menubar [items]="menu" (itemClick)="run($event)" />
`,
})
export class Demo {
protected readonly menu: OgeMenubarItemData[] = [
{
text: 'File',
items: [
// shortcut renders right-aligned and announces aria-keyshortcuts;
// the actual key binding stays the application's job.
{ text: 'New', key: 'new', shortcut: 'Ctrl+N' },
{ text: 'Open…', key: 'open', shortcut: 'Ctrl+O' },
{ separator: true, text: '' },
{ text: 'Share', badge: 2, items: [{ text: 'Email', key: 'email' }] },
],
},
{ text: 'Edit', items: [{ text: 'Undo', key: 'undo', shortcut: 'Ctrl+Z' }] },
{ text: 'Help', key: 'help' },
];
protected run(event: OgeMenubarItemClickEvent): void {
console.log(event.key, event.path);
}
}Declarative items
Both APIs, one rule: declarative children render first, then the items input — the house merge order shared with the toolbar and the button group. Nesting <oge-menubar-item> builds the submenu tree.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar, OgeMenubarItem } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar, OgeMenubarItem],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Declarative children come first, then the items input — the
house merge order. Nesting oge-menubar-item builds the submenu tree. -->
<oge-menubar>
<oge-menubar-item text="File">
<oge-menubar-item text="New" key="new" />
<oge-menubar-item [separator]="true" />
<oge-menubar-item text="Exit" key="exit" />
</oge-menubar-item>
<oge-menubar-item text="Help" key="help" />
</oge-menubar>
`,
})
export class Demo {}Open mode
openMode governs the top level only: click (default, the desktop convention) or hover after hoverDelay. Nested levels always open on hover and ArrowRight — DevExtreme's showFirstSubmenuMode/showSubmenuMode split collapsed into behavior. With a menu open, hovering siblings switches in either mode.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar } from '@oge-ui/navigation';
import type { OgeMenubarItemData } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- openMode applies to the TOP level only: 'click' is the
desktop-menubar default, 'hover' opens after hoverDelay. Nested levels
always open on hover and on ArrowRight/Enter — the reference libraries'
first-vs-nested split baked in as behavior, not a second input. Once a
menu is open, hovering siblings switches it in either mode. -->
<oge-menubar [items]="menu" openMode="hover" [hoverDelay]="150" />
`,
})
export class Demo {
protected readonly menu: OgeMenubarItemData[] = [
{ text: 'File', items: [{ text: 'New' }] },
{ text: 'Edit', items: [{ text: 'Undo' }] },
];
}Vertical menubar
Same widget, same roles: aria-orientation="vertical" is announced, Up/Down traverse the bar and ArrowRight opens the submenu beside it — the axis swap the APG prescribes.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar } from '@oge-ui/navigation';
import type { OgeMenubarItemData } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A vertical menubar keeps role="menubar" and announces
aria-orientation="vertical"; Up/Down traverse the bar and ArrowRight
opens the submenu beside it, exactly as the APG allows. -->
<oge-menubar orientation="vertical" [items]="menu" />
`,
})
export class Demo {
protected readonly menu: OgeMenubarItemData[] = [
{ text: 'Dashboard', key: 'dashboard' },
{ text: 'Reports', items: [{ text: 'Monthly' }, { text: 'Annual' }] },
{ text: 'Settings', items: [{ text: 'Profile' }] },
];
}Adaptive hamburger
compactBelow measures the menubar's own container, never the window — a bar inside a split pane adapts to the room it actually has. Below the threshold the whole bar becomes one hamburger button opening the full tree as nested menus. Drag the range to squeeze it.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar } from '@oge-ui/navigation';
import type { OgeMenubarItemData, OgeMenubarCompactChangedEvent } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- compactBelow measures the menubar's OWN container, never the
window (DevExtreme collapses on widget overflow, PrimeNG on a media
query). Below the threshold the whole bar becomes a hamburger button
opening the full tree as one nested menu — no second interaction model. -->
<oge-menubar
[items]="menu"
[compactBelow]="480"
(compactChanged)="onCompact($event)"
/>
`,
})
export class Demo {
protected readonly menu: OgeMenubarItemData[] = [
{ text: 'File', items: [{ text: 'New' }] },
{ text: 'Edit', items: [{ text: 'Undo' }] },
{ text: 'Help', key: 'help' },
];
protected onCompact(event: OgeMenubarCompactChangedEvent): void {
console.log('compact:', event.compact);
}
}Cancelable events
The -ing pair carries the house mutable cancel flag. Closes the menubar itself initiates (escape, select, navigation, api) are interceptable; pointer closes owned by the overlay (outside) and Tab only report submenuClosed. Lock the menu and try to open or Escape it.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeMenubar } from '@oge-ui/navigation';
import type { OgeMenubarItemData, OgeMenubarSubmenuOpeningEvent, OgeMenubarSubmenuClosingEvent } from '@oge-ui/navigation';
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The -ing pair is cancelable with the house mutable cancel
flag. Closes the menubar initiates (escape, select, navigation, api) run
through submenuClosing; pointer closes owned by the overlay (outside)
and Tab only report submenuClosed. -->
<oge-menubar
[items]="menu"
(submenuOpening)="onOpening($event)"
(submenuClosing)="onClosing($event)"
/>
`,
})
export class Demo {
protected readonly locked = signal(false);
protected readonly menu: OgeMenubarItemData[] = [
{ text: 'File', key: 'file', items: [{ text: 'New' }] },
];
protected onOpening(event: OgeMenubarSubmenuOpeningEvent): void {
if (this.locked()) event.cancel = true;
}
protected onClosing(event: OgeMenubarSubmenuClosingEvent): void {
if (this.locked() && event.reason !== 'tab') event.cancel = true;
}
}Configuration
Suite-wide defaults for openMode, hoverDelay, orientation and compactBelow, plus every user-facing string — the bar's accessible name and the hamburger label included — via provideOgeMenubarConfig().
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeMenubar, provideOgeMenubarConfig } from '@oge-ui/navigation';
import type { OgeMenubarItemData } from '@oge-ui/navigation';
// Every user-facing string lives in the messages block — the bar's
// accessible name and the compact hamburger's label included.
export const MENUBAR_PROVIDERS = [
provideOgeMenubarConfig({
openMode: 'hover',
compactBelow: 480,
messages: { menubar: 'Ana menü', hamburger: 'Menü' },
}),
];
@Component({
selector: 'demo-root',
imports: [OgeMenubar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-menubar [items]="menu" />
`,
})
export class Demo {
protected readonly menu: OgeMenubarItemData[] = [
{ text: 'Dosya', items: [{ text: 'Yeni' }] },
];
}