Skip to content
Ndop UI

Menubar

A bar of menus, such as a desktop app's File and Edit menus.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  Menubar,
  MenubarCheckboxItem,
  MenubarContent,
  MenubarGroup,
  MenubarItem,
  MenubarItemIndicator,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarShortcut,
  MenubarSubContent,
  MenubarSubTrigger,
  MenubarTrigger,
} from '@/ui/menubar/menubar';

/**
 * An editor's File, Edit, View and Profiles menus, with shortcuts, a
 * disabled item, a submenu, checkbox items and a radio group. The menubar
 * is one tab stop; the arrow keys move between its menus.
 */
@Component({
  selector: 'example-menubar-demo',
  imports: [
    Menubar,
    MenubarCheckboxItem,
    MenubarContent,
    MenubarGroup,
    MenubarItem,
    MenubarItemIndicator,
    MenubarRadioGroup,
    MenubarRadioItem,
    MenubarSeparator,
    MenubarShortcut,
    MenubarSubContent,
    MenubarSubTrigger,
    MenubarTrigger,
    NdIcon,
  ],
  template: `
    <div class="grid justify-items-start gap-2">
      <div ndMenubar aria-label="Editor">
        <div [ndMenubarTrigger]="file">File</div>
        <div [ndMenubarTrigger]="edit">Edit</div>
        <div [ndMenubarTrigger]="view">View</div>
        <div [ndMenubarTrigger]="profiles">Profiles</div>
      </div>
      <p class="text-sm text-muted-foreground" aria-live="polite">
        {{ chosen() }}
      </p>
    </div>
    <ng-template #file>
      <div ndMenubarContent>
        <div ndMenubarItem (triggered)="choose('New tab')">
          New tab <span ndMenubarShortcut>⌘T</span>
        </div>
        <div ndMenubarItem (triggered)="choose('New window')">
          New window <span ndMenubarShortcut>⌘N</span>
        </div>
        <div ndMenubarItem disabled>New incognito window</div>
        <div ndMenubarSeparator></div>
        <div [ndMenubarSubTrigger]="share">
          Share
          <svg
            ndIcon="chevron-right"
            class="ms-auto size-4 rtl:rotate-180"
          ></svg>
        </div>
        <div ndMenubarSeparator></div>
        <div ndMenubarItem (triggered)="choose('Print')">
          Print… <span ndMenubarShortcut>⌘P</span>
        </div>
      </div>
    </ng-template>
    <ng-template #share>
      <div ndMenubarSubContent>
        <div ndMenubarItem (triggered)="choose('Email link')">Email link</div>
        <div ndMenubarItem (triggered)="choose('Messages')">Messages</div>
        <div ndMenubarItem (triggered)="choose('Notes')">Notes</div>
      </div>
    </ng-template>
    <ng-template #edit>
      <div ndMenubarContent>
        <div ndMenubarItem (triggered)="choose('Undo')">
          Undo <span ndMenubarShortcut>⌘Z</span>
        </div>
        <div ndMenubarItem (triggered)="choose('Redo')">
          Redo <span ndMenubarShortcut>⇧⌘Z</span>
        </div>
        <div ndMenubarSeparator></div>
        <div ndMenubarItem (triggered)="choose('Cut')">Cut</div>
        <div ndMenubarItem (triggered)="choose('Copy')">Copy</div>
        <div ndMenubarItem (triggered)="choose('Paste')">Paste</div>
      </div>
    </ng-template>
    <ng-template #view>
      <div ndMenubarContent>
        <div ndMenubarCheckboxItem [(checked)]="bookmarks">
          <span ndMenubarItemIndicator><svg ndIcon="check"></svg></span>
          Always show bookmarks bar
        </div>
        <div ndMenubarCheckboxItem [(checked)]="fullUrls">
          <span ndMenubarItemIndicator><svg ndIcon="check"></svg></span>
          Always show full URLs
        </div>
        <div ndMenubarSeparator></div>
        <div ndMenubarItem inset (triggered)="choose('Reload')">
          Reload <span ndMenubarShortcut>⌘R</span>
        </div>
        <div ndMenubarItem inset disabled>
          Force reload <span ndMenubarShortcut>⇧⌘R</span>
        </div>
        <div ndMenubarSeparator></div>
        <div ndMenubarItem inset (triggered)="choose('Toggle full screen')">
          Toggle full screen
        </div>
      </div>
    </ng-template>
    <ng-template #profiles>
      <div ndMenubarContent>
        <div ndMenubarRadioGroup aria-label="Profile" [(value)]="profile">
          @for (name of names; track name) {
            <div ndMenubarRadioItem [value]="name">
              <span ndMenubarItemIndicator>
                <svg ndIcon="circle" class="size-2 fill-current"></svg>
              </span>
              {{ name }}
            </div>
          }
        </div>
        <div ndMenubarSeparator></div>
        <div ndMenubarGroup aria-label="Manage">
          <div ndMenubarItem inset (triggered)="choose('Edit profiles')">
            Edit…
          </div>
          <div ndMenubarItem inset (triggered)="choose('Add profile')">
            Add profile…
          </div>
        </div>
      </div>
    </ng-template>
  `,
})
export class MenubarDemoExample {
  protected readonly chosen = signal('');
  protected readonly bookmarks = signal(false);
  protected readonly fullUrls = signal(true);
  protected readonly profile = signal<string | null>('Benoit');
  protected readonly names = ['Andy', 'Benoit', 'Luis'];

  /**
   * Reports what was chosen.
   *
   * @param action - The item's name.
   */
  protected choose(action: string): void {
    this.chosen.set(`Chose ${action}.`);
  }
}

A menubar is a row of menus, like a desktop app's File, Edit and View menus. It's one tab stop: the arrow keys move between its menus, and open the next one while a menu is open. Its menus have the same items as Dropdown Menu: checkboxes, radios, submenus, labels, separators and shortcuts.

Installation

npx @ndop/cli add menubar

Usage

import {
  Menubar,
  MenubarCheckboxItem,
  MenubarContent,
  MenubarGroup,
  MenubarItem,
  MenubarItemIndicator,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarShortcut,
  MenubarSubContent,
  MenubarSubTrigger,
  MenubarTrigger,
} from '@/ui/menubar/menubar';
<div class="grid justify-items-start gap-2">
  <div ndMenubar aria-label="Editor">
    <div [ndMenubarTrigger]="file">File</div>
    <div [ndMenubarTrigger]="edit">Edit</div>
    <div [ndMenubarTrigger]="view">View</div>
    <div [ndMenubarTrigger]="profiles">Profiles</div>
  </div>
  <p class="text-sm text-muted-foreground" aria-live="polite">
    {{ chosen() }}
  </p>
</div>
<ng-template #file>
  <div ndMenubarContent>
    <div ndMenubarItem (triggered)="choose('New tab')">
      New tab <span ndMenubarShortcut>⌘T</span>
    </div>
    <div ndMenubarItem (triggered)="choose('New window')">
      New window <span ndMenubarShortcut>⌘N</span>
    </div>
    <div ndMenubarItem disabled>New incognito window</div>
    <div ndMenubarSeparator></div>
    <div [ndMenubarSubTrigger]="share">
      Share
      <svg
        ndIcon="chevron-right"
        class="ms-auto size-4 rtl:rotate-180"
      ></svg>
    </div>
    <div ndMenubarSeparator></div>
    <div ndMenubarItem (triggered)="choose('Print')">
      Print… <span ndMenubarShortcut>⌘P</span>
    </div>
  </div>
</ng-template>
<ng-template #share>
  <div ndMenubarSubContent>
    <div ndMenubarItem (triggered)="choose('Email link')">Email link</div>
    <div ndMenubarItem (triggered)="choose('Messages')">Messages</div>
    <div ndMenubarItem (triggered)="choose('Notes')">Notes</div>
  </div>
</ng-template>
<ng-template #edit>
  <div ndMenubarContent>
    <div ndMenubarItem (triggered)="choose('Undo')">
      Undo <span ndMenubarShortcut>⌘Z</span>
    </div>
    <div ndMenubarItem (triggered)="choose('Redo')">
      Redo <span ndMenubarShortcut>⇧⌘Z</span>
    </div>
    <div ndMenubarSeparator></div>
    <div ndMenubarItem (triggered)="choose('Cut')">Cut</div>
    <div ndMenubarItem (triggered)="choose('Copy')">Copy</div>
    <div ndMenubarItem (triggered)="choose('Paste')">Paste</div>
  </div>
</ng-template>
<ng-template #view>
  <div ndMenubarContent>
    <div ndMenubarCheckboxItem [(checked)]="bookmarks">
      <span ndMenubarItemIndicator><svg ndIcon="check"></svg></span>
      Always show bookmarks bar
    </div>
    <div ndMenubarCheckboxItem [(checked)]="fullUrls">
      <span ndMenubarItemIndicator><svg ndIcon="check"></svg></span>
      Always show full URLs
    </div>
    <div ndMenubarSeparator></div>
    <div ndMenubarItem inset (triggered)="choose('Reload')">
      Reload <span ndMenubarShortcut>⌘R</span>
    </div>
    <div ndMenubarItem inset disabled>
      Force reload <span ndMenubarShortcut>⇧⌘R</span>
    </div>
    <div ndMenubarSeparator></div>
    <div ndMenubarItem inset (triggered)="choose('Toggle full screen')">
      Toggle full screen
    </div>
  </div>
</ng-template>
<ng-template #profiles>
  <div ndMenubarContent>
    <div ndMenubarRadioGroup aria-label="Profile" [(value)]="profile">
      @for (name of names; track name) {
        <div ndMenubarRadioItem [value]="name">
          <span ndMenubarItemIndicator>
            <svg ndIcon="circle" class="size-2 fill-current"></svg>
          </span>
          {{ name }}
        </div>
      }
    </div>
    <div ndMenubarSeparator></div>
    <div ndMenubarGroup aria-label="Manage">
      <div ndMenubarItem inset (triggered)="choose('Edit profiles')">
        Edit…
      </div>
      <div ndMenubarItem inset (triggered)="choose('Add profile')">
        Add profile…
      </div>
    </div>
  </div>
</ng-template>

Accessibility

Implements the Menubar pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
TabMoves focus into the menubar, to its first trigger; from a trigger, out of the menubar.
ArrowRightorArrowLeftMoves focus to the next or previous trigger, wrapping; if a menu is open, opens that trigger's menu instead.
ArrowDownOn a trigger, opens its menu and focuses its first item.
EnterorSpaceOn a trigger, opens its menu and focuses its first item.
EscapeCloses the open menu and returns focus to its trigger.
ArrowDownorArrowUpMoves focus to the next or previous item, wrapping.
HomeorEndMoves focus to the first or last item.
EnterActivates the focused item and closes the menu.
SpaceActivates the focused item; checkbox and radio items keep the menu open.
ArrowRightOn a sub-trigger, opens its menu and focuses the first item.
ArrowLeftIn a submenu, closes it and focuses its sub-trigger.
EscapeCloses the current menu and returns focus to what opened it.
TabCloses every open menu and moves focus on from the trigger.
aTypeahead: moves focus to the next item whose label starts with the typed letters.

Focus

  • The menubar is one tab stop, whose focus goes to its first trigger.
  • Closing a menu returns focus to its trigger.
  • While one menu is open, hovering another trigger opens its menu.
  • Opened from the keyboard, a menu focuses its first item (its last with ArrowUp on a Dropdown Menu trigger); opened with the pointer, the menu itself.
  • Closing returns focus to the trigger or sub-trigger.

Right-to-left

In right-to-left layouts, the bar runs from right to left: ArrowLeft moves to the next trigger and ArrowRight to the previous one, and menus align to the trigger's right edge. Direction comes from the app's Directionality: the document's dir, or a CDK [dir] directive. ArrowLeft opens submenus and ArrowRight closes them, and submenus open on the left. Direction comes from the app's Directionality: the document's dir, or a CDK [dir] directive.

API reference

Menubar

[ndMenubar]role menubar

A menubar (@ndop/primitives/menubar): a bar of menus, such as a desktop app's File and Edit menus. It's one tab stop; the arrow keys move between its triggers, and between their menus while one is open. Name it with aria-label.

Manages aria-orientation, tabindex.

Menubar binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar

MenubarTrigger

[ndMenubarTrigger]role menuitem

One menu of a menubar: its value is the menu's ng-template, which opens below it. Use a div.

Manages aria-haspopup, aria-expanded, aria-controls.

MenubarTrigger binding Type Default
[class]
Extra classes, merged last so they win.
string''
[ndMenubarTrigger]
The ng-template of the menu it opens.
TemplateRef<unknown> | nullnull
[side]
Side of the trigger the menu opens on, before flipping to fit.
'bottom' | 'top' | 'start' | 'end''bottom'
[align]
Alignment of the menu along that side.
'start' | 'end' | 'center''start'
[sideOffset]
Gap between the trigger and the menu, in pixels.
number8
Data attribute Values
data-slotmenubar-trigger
data-state —

MenubarContent

[ndMenubarContent]role menu

A menubar menu's list (@ndop/primitives/menu), in the ng-template a [ndMenubarTrigger] button opens. Arrow keys, Home, End and typing move between items; Escape closes it and returns focus to its trigger.

Manages aria-orientation.

MenubarContent binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-content
data-state —
data-side —

MenubarSubContent

[ndMenubarSubContent]role menu

A submenu, opened beside its sub-trigger: at the end side, so on the left in right-to-left layouts.

Manages aria-orientation.

MenubarSubContent binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-sub-content
data-state —
data-side —

MenubarGroup

[ndMenubarGroup]role group

Groups related items (role="group"). Name it with aria-labelledby pointing at a label.

Data attribute Values
data-slotmenubar-group

MenubarItem

[ndMenubarItem]role menuitem

One action (role="menuitem"): activating it emits triggered and closes the menu. Disabled items stay focusable and do nothing. Use a div: on a button, Space would close the menu.

Manages aria-disabled.

MenubarItem binding Type Default
[inset]
Indents it to line up with items that have an indicator.
booleanfalse
[variant]
destructive colors a dangerous action, such as Delete.
'default' | 'destructive''default'
[class]
Extra classes, merged last so they win.
string''
[disabled]
Makes it inert but focusable, with aria-disabled.
booleanfalse
Data attribute Values
data-slotmenubar-item
data-inset —
data-variant —
data-disabled —

MenubarCheckboxItem

[ndMenubarCheckboxItem]role menuitemcheckbox

An item that is on or off (role="menuitemcheckbox"), bound with [(checked)]. Put an indicator with a check icon in it.

Manages aria-checked, aria-disabled.

MenubarCheckboxItem binding Type Default
[class]
Extra classes, merged last so they win.
string''
[disabled]
Makes it inert but focusable, with aria-disabled.
booleanfalse
[(checked)]
Whether it's on. Two-way bindable with [(checked)].
booleanfalse
Data attribute Values
data-slotmenubar-checkbox-item
data-state —
data-disabled —

MenubarRadioGroup

[ndMenubarRadioGroup]role group

Groups radio items (role="group") and holds the checked value, bound with [(value)].

MenubarRadioGroup binding Type Default
[(value)]
The checked radio item's value, or null. Two-way bindable with [(value)].
string | nullnull
Data attribute Values
data-slotmenubar-radio-group

MenubarRadioItem

[ndMenubarRadioItem]role menuitemradio

One choice of a radio group (role="menuitemradio"). Put an indicator with a small filled circle in it.

Manages aria-checked, aria-disabled.

MenubarRadioItem binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]required
The value its group takes when it's checked.
string—
[disabled]
Makes it inert but focusable, with aria-disabled.
booleanfalse
Data attribute Values
data-slotmenubar-radio-item
data-state —
data-disabled —

MenubarItemIndicator

[ndMenubarItemIndicator]

The check or dot at the start of a checkbox or radio item, shown while it is checked.

MenubarItemIndicator binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-item-indicator
data-state —

MenubarLabel

[ndMenubarLabel]

A heading for a group of items; point the group's aria-labelledby at it.

MenubarLabel binding Type Default
[inset]
Indents it to line up with items that have an indicator.
booleanfalse
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-label
data-inset —

MenubarSeparator

[ndMenubarSeparator]role separator

A line between groups of items (role="separator").

MenubarSeparator binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-separator

MenubarShortcut

[ndMenubarShortcut]

The keyboard shortcut of an item, at its end, as a reminder; the menu doesn't handle it.

MenubarShortcut binding Type Default
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slotmenubar-shortcut

MenubarSubTrigger

[ndMenubarSubTrigger]role menuitem

An item that opens the submenu in its template. Put a chevron icon at its end with ms-auto, turned around in right-to-left layouts.

Manages aria-haspopup, aria-expanded, aria-controls.

MenubarSubTrigger binding Type Default
[inset]
Indents it to line up with items that have an indicator.
booleanfalse
[class]
Extra classes, merged last so they win.
string''
[ndMenubarSubTrigger]
The ng-template of the submenu.
TemplateRef<unknown> | nullnull
[disabled]
Makes it inert but focusable, with aria-disabled.
booleanfalse
Data attribute Values
data-slotmenubar-sub-trigger
data-inset —
data-state —
data-disabled —

Agent notes

Do

  • Use it in app-like screens, such as an editor's toolbar.
  • Name it with aria-label.
  • Show keyboard shortcuts the app really binds.

Don't

  • Don't use it for site navigation; use Navigation Menu.
  • Don't use it for one menu; use Dropdown Menu.