Menubar
A bar of menus, such as a desktop app's File and Edit menus.
- v1.0.0
- WAI-ARIA: Menubar
- SSR
- Zoneless
- 3.4 kB gzip
- +19.9 kB Angular CDK, shared
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 menubarpnpm dlx @ndop/cli add menubaryarn dlx @ndop/cli add menubarbunx @ndop/cli add menubarInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/menubar/menubar.tsimport { booleanAttribute, computed, Directive, input } from '@angular/core'; import { NdMenuContent, NdMenuGroup, NdMenuItem, NdMenuItemCheckbox, NdMenuItemIndicator, NdMenuItemRadio, NdMenuLabel, NdMenuRadioGroup, NdMenuSeparator, NdMenuSubTrigger, } from '@ndop/primitives/menu'; import { NdMenubar, NdMenubarTrigger } from '@ndop/primitives/menubar'; import { cn } from '@/lib/utils'; /** * 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`. * * @example * ```html * <div ndMenubar aria-label="Editor"> * <div [ndMenubarTrigger]="file">File</div> * </div> * ``` */ @Directive({ selector: '[ndMenubar]', hostDirectives: [NdMenubar], host: { 'data-slot': 'menubar', '[class]': 'classes()' }, }) export class Menubar { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-xs', this.class(), ), ); } /** * One menu of a menubar: its value is the menu's `ng-template`, which opens * below it. Use a `div`. * * @example * ```html * <div [ndMenubarTrigger]="file">File</div> * ``` */ @Directive({ selector: '[ndMenubarTrigger]', hostDirectives: [ { directive: NdMenubarTrigger, inputs: ['ndMenubarTrigger', 'side', 'align', 'sideOffset'], }, ], host: { 'data-slot': 'menubar-trigger', '[class]': 'classes()' }, }) export class MenubarTrigger { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground', this.class(), ), ); } /** * 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. * * @example * ```html * <div ndMenubarContent><div ndMenubarItem>Profile</div></div> * ``` */ @Directive({ selector: '[ndMenubarContent]', hostDirectives: [{ directive: NdMenuContent, outputs: ['closed'] }], host: { 'data-slot': 'menubar-content', '[class]': 'classes()' }, }) export class MenubarContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', this.class(), ), ); } /** * A submenu, opened beside its sub-trigger: at the end side, so on the left * in right-to-left layouts. * * @example * ```html * <div ndMenubarSubContent>…</div> * ``` */ @Directive({ selector: '[ndMenubarSubContent]', hostDirectives: [{ directive: NdMenuContent, outputs: ['closed'] }], host: { 'data-slot': 'menubar-sub-content', '[class]': 'classes()' }, }) export class MenubarSubContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', this.class(), ), ); } /** * Groups related items (`role="group"`). Name it with `aria-labelledby` * pointing at a label. * * @example * ```html * <div ndMenubarGroup aria-labelledby="account">…</div> * ``` */ @Directive({ selector: '[ndMenubarGroup]', hostDirectives: [NdMenuGroup], host: { 'data-slot': 'menubar-group' }, }) export class MenubarGroup {} /** * 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. * * @example * ```html * <div ndMenubarItem (triggered)="edit()">Edit</div> * ``` */ @Directive({ selector: '[ndMenubarItem]', hostDirectives: [ { directive: NdMenuItem, inputs: ['disabled'], outputs: ['triggered'] }, ], host: { 'data-slot': 'menubar-item', '[attr.data-inset]': 'inset() ? "" : null', '[attr.data-variant]': 'variant()', '[class]': 'classes()', }, }) export class MenubarItem { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** `destructive` colors a dangerous action, such as Delete. */ readonly variant = input<'default' | 'destructive'>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", this.class(), ), ); } /** * An item that is on or off (`role="menuitemcheckbox"`), bound with * `[(checked)]`. Put an indicator with a check icon in it. * * @example * ```html * <div ndMenubarCheckboxItem [(checked)]="statusBar">…</div> * ``` */ @Directive({ selector: '[ndMenubarCheckboxItem]', hostDirectives: [ { directive: NdMenuItemCheckbox, inputs: ['checked', 'disabled'], outputs: ['checkedChange', 'triggered'], }, ], host: { 'data-slot': 'menubar-checkbox-item', '[class]': 'classes()' }, }) export class MenubarCheckboxItem { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", this.class(), ), ); } /** * Groups radio items (`role="group"`) and holds the checked value, bound * with `[(value)]`. * * @example * ```html * <div ndMenubarRadioGroup aria-label="Panel position" [(value)]="position">…</div> * ``` */ @Directive({ selector: '[ndMenubarRadioGroup]', hostDirectives: [ { directive: NdMenuRadioGroup, inputs: ['value'], outputs: ['valueChange'], }, ], host: { 'data-slot': 'menubar-radio-group' }, }) export class MenubarRadioGroup {} /** * One choice of a radio group (`role="menuitemradio"`). Put an indicator * with a small filled circle in it. * * @example * ```html * <div ndMenubarRadioItem value="top">Top</div> * ``` */ @Directive({ selector: '[ndMenubarRadioItem]', hostDirectives: [ { directive: NdMenuItemRadio, inputs: ['value', 'disabled'], outputs: ['triggered'], }, ], host: { 'data-slot': 'menubar-radio-item', '[class]': 'classes()' }, }) export class MenubarRadioItem { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", this.class(), ), ); } /** * The check or dot at the start of a checkbox or radio item, shown while it * is checked. * * @example * ```html * <span ndMenubarItemIndicator><svg ndIcon="check"></svg></span> * ``` */ @Directive({ selector: '[ndMenubarItemIndicator]', hostDirectives: [NdMenuItemIndicator], host: { 'data-slot': 'menubar-item-indicator', '[class]': 'classes()' }, }) export class MenubarItemIndicator { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'pointer-events-none absolute start-2 flex size-3.5 items-center justify-center', this.class(), ), ); } /** * A heading for a group of items; point the group's `aria-labelledby` at * it. * * @example * ```html * <div ndMenubarLabel id="account">My account</div> * ``` */ @Directive({ selector: '[ndMenubarLabel]', hostDirectives: [NdMenuLabel], host: { 'data-slot': 'menubar-label', '[attr.data-inset]': 'inset() ? "" : null', '[class]': 'classes()', }, }) export class MenubarLabel { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('px-2 py-1.5 text-sm font-medium data-[inset]:ps-8', this.class()), ); } /** * A line between groups of items (`role="separator"`). * * @example * ```html * <div ndMenubarSeparator></div> * ``` */ @Directive({ selector: '[ndMenubarSeparator]', hostDirectives: [NdMenuSeparator], host: { 'data-slot': 'menubar-separator', '[class]': 'classes()' }, }) export class MenubarSeparator { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('-mx-1 my-1 h-px bg-border', this.class()), ); } /** * The keyboard shortcut of an item, at its end, as a reminder; the menu * doesn't handle it. * * @example * ```html * <span ndMenubarShortcut>⌘S</span> * ``` */ @Directive({ selector: '[ndMenubarShortcut]', host: { 'data-slot': 'menubar-shortcut', '[class]': 'classes()' }, }) export class MenubarShortcut { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('ms-auto text-xs tracking-widest text-muted-foreground', this.class()), ); } /** * 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. * * @example * ```html * <div [ndMenubarSubTrigger]="more">More tools</div> * ``` */ @Directive({ selector: '[ndMenubarSubTrigger]', hostDirectives: [ { directive: NdMenuSubTrigger, inputs: ['ndMenuSubTrigger: ndMenubarSubTrigger', 'disabled'], }, ], host: { 'data-slot': 'menubar-sub-trigger', '[attr.data-inset]': 'inset() ? "" : null', '[class]': 'classes()', }, }) export class MenubarSubTrigger { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:ps-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
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 |
|---|---|
| Tab | Moves focus into the menubar, to its first trigger; from a trigger, out of the menubar. |
| ArrowRightorArrowLeft | Moves focus to the next or previous trigger, wrapping; if a menu is open, opens that trigger's menu instead. |
| ArrowDown | On a trigger, opens its menu and focuses its first item. |
| EnterorSpace | On a trigger, opens its menu and focuses its first item. |
| Escape | Closes the open menu and returns focus to its trigger. |
| ArrowDownorArrowUp | Moves focus to the next or previous item, wrapping. |
| HomeorEnd | Moves focus to the first or last item. |
| Enter | Activates the focused item and closes the menu. |
| Space | Activates the focused item; checkbox and radio items keep the menu open. |
| ArrowRight | On a sub-trigger, opens its menu and focuses the first item. |
| ArrowLeft | In a submenu, closes it and focuses its sub-trigger. |
| Escape | Closes the current menu and returns focus to what opened it. |
| Tab | Closes every open menu and moves focus on from the trigger. |
| a | Typeahead: 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
ArrowUpon 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 menubarA 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-slot | menubar |
MenubarTrigger
[ndMenubarTrigger]role menuitemOne 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> | null | null |
[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. | number | 8 |
| Data attribute | Values |
|---|---|
data-slot | menubar-trigger |
data-state | — |
MenubarContent
[ndMenubarContent]role menuA 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-slot | menubar-content |
data-state | — |
data-side | — |
MenubarSubContent
[ndMenubarSubContent]role menuA 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-slot | menubar-sub-content |
data-state | — |
data-side | — |
MenubarGroup
[ndMenubarGroup]role groupGroups related items (role="group"). Name it with aria-labelledby
pointing at a label.
| Data attribute | Values |
|---|---|
data-slot | menubar-group |
MenubarItem
[ndMenubarItem]role menuitemOne 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. | boolean | false |
[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. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | menubar-item |
data-inset | — |
data-variant | — |
data-disabled | — |
MenubarCheckboxItem
[ndMenubarCheckboxItem]role menuitemcheckboxAn 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. | boolean | false |
[(checked)]Whether it's on. Two-way bindable with [(checked)]. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | menubar-checkbox-item |
data-state | — |
data-disabled | — |
MenubarRadioGroup
[ndMenubarRadioGroup]role groupGroups 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 | null | null |
| Data attribute | Values |
|---|---|
data-slot | menubar-radio-group |
MenubarRadioItem
[ndMenubarRadioItem]role menuitemradioOne 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]requiredThe value its group takes when it's checked. | string | — |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | menubar-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-slot | menubar-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. | boolean | false |
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | menubar-label |
data-inset | — |
MenubarSeparator
[ndMenubarSeparator]role separatorA line between groups of items (role="separator").
| MenubarSeparator binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | menubar-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-slot | menubar-shortcut |
MenubarSubTrigger
[ndMenubarSubTrigger]role menuitemAn 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. | boolean | false |
[class]Extra classes, merged last so they win. | string | '' |
[ndMenubarSubTrigger]The ng-template of the submenu. | TemplateRef<unknown> | null | null |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | menubar-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.