Menu
Overview
A menu offers a list of actions or options to users, typically appearing in response to a button click or right-click. Menus support keyboard navigation with arrow keys, submenus, checkboxes, radio buttons, and disabled items.
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; color: var(--primary-contrast); border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin: 0; width: 15rem; padding: 0.25rem; border-radius: 0.5rem; border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem; border-radius: 0.25rem;}[ngMenuTrigger]:hover,[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--border-color); margin: 0.25rem 0; opacity: 0.25;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </div> <div class="group"> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </div> </ng-template> </div> </ng-template> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; border: 1px solid transparent; background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent); color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { gap: 3px; width: 15rem; display: flex; flex-direction: column;}[ngMenu] .group { padding: 0.25rem; border-radius: 0.25rem; background-color: var(--page-background); box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);}[ngMenu] .group:first-of-type { border-top-left-radius: 1rem; border-top-right-radius: 1rem;}[ngMenu] .group:last-of-type { border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.75rem; font-size: 0.875rem; border-radius: 0.75rem;}[ngMenuTrigger]:hover,[ngMenuTrigger][aria-expanded='true'] { background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);}[ngMenuItem][data-active='true'] { color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast)); background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--gray-500); margin: 0.25rem 0; opacity: 0.25;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()"> Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-size: 0.8rem; --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background)); font-family: 'Press Start 2P'; --retro-button-color: var(--vivid-pink); --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent); --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; font-family: 'Press Start 2P'; color: #000; background-color: var(--vivid-pink); box-shadow: var(--retro-clickable-shadow); transition: transform 0.1s, box-shadow 0.1s;}[ngMenuTrigger]:hover { transform: translate(1px, 1px);}[ngMenuTrigger]:active { background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff); box-shadow: var(--retro-pressed-shadow); transform: translate(4px, 4px);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin-top: 8px; width: 15rem; padding: 0.25rem; background-color: var(--page-background); box-shadow: var(--retro-flat-shadow);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem;}[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuTrigger]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: 8px;}[ngMenuItem]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: -4px;}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 4px solid #000; margin: 0.25rem 0; opacity: 0.25;}
Usage
Menus work well for presenting lists of actions or commands that users can choose from.
Use menus when:
- Building application command menus (File, Edit, View)
- Creating context menus (right-click actions)
- Showing dropdown action lists
- Implementing toolbar dropdowns
- Organizing settings or options
Avoid menus when:
- Building site navigation (use navigation landmarks instead)
- Creating form selects (use the Select component)
- Switching between content panels (use Tabs)
- Showing collapsible content (use Accordion)
Features
- Keyboard navigation - Arrow keys, Home/End, and character search for efficient navigation
- Submenus - Nested menu support with automatic positioning
- Menu types - Standalone menus, triggered menus, and menubars
- Checkboxes and radios - Toggle and selection menu items
- Disabled items - Soft or hard disabled states with focus management
- Auto-close behavior - Configurable close on selection
- RTL support - Right-to-left language navigation
Examples
Menu with trigger
Create a dropdown menu by pairing a trigger button with a menu. The trigger opens and closes the menu.
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; color: var(--primary-contrast); border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin: 0; width: 15rem; padding: 0.25rem; border-radius: 0.5rem; border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem; border-radius: 0.25rem;}[ngMenuTrigger]:hover,[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--border-color); margin: 0.25rem 0; opacity: 0.25;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </div> <div class="group"> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </div> </ng-template> </div> </ng-template> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; border: 1px solid transparent; background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent); color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { gap: 3px; width: 15rem; display: flex; flex-direction: column;}[ngMenu] .group { padding: 0.25rem; border-radius: 0.25rem; background-color: var(--page-background); box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);}[ngMenu] .group:first-of-type { border-top-left-radius: 1rem; border-top-right-radius: 1rem;}[ngMenu] .group:last-of-type { border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.75rem; font-size: 0.875rem; border-radius: 0.75rem;}[ngMenuTrigger]:hover,[ngMenuTrigger][aria-expanded='true'] { background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);}[ngMenuItem][data-active='true'] { color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast)); background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--gray-500); margin: 0.25rem 0; opacity: 0.25;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()"> Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-size: 0.8rem; --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background)); font-family: 'Press Start 2P'; --retro-button-color: var(--vivid-pink); --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent); --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; font-family: 'Press Start 2P'; color: #000; background-color: var(--vivid-pink); box-shadow: var(--retro-clickable-shadow); transition: transform 0.1s, box-shadow 0.1s;}[ngMenuTrigger]:hover { transform: translate(1px, 1px);}[ngMenuTrigger]:active { background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff); box-shadow: var(--retro-pressed-shadow); transform: translate(4px, 4px);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin-top: 8px; width: 15rem; padding: 0.25rem; background-color: var(--page-background); box-shadow: var(--retro-flat-shadow);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem;}[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuTrigger]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: 8px;}[ngMenuItem]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: -4px;}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 4px solid #000; margin: 0.25rem 0; opacity: 0.25;}
The menu automatically closes when a user selects an item or presses Escape.
Context menu
Context menus appear at the cursor position when users right-click an element.
app.ts
import {Component} from '@angular/core';@Component({ selector: 'app-root', templateUrl: './app.html', styleUrl: './app.css',})export class App {}
app.html
<h1>Coming Soon</h1>
Position the menu using the contextmenu event coordinates.
Standalone menu
A standalone menu doesn't require a trigger and remains visible in the interface.
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, OverlayModule],})export class App { updateMenu = viewChild<Menu<string>>('updateMenu');}
app.html
<div ngMenu> <ng-template ngMenuContent> <span id="security-label" class="heading">SECURITY</span> <div role="group" aria-labelledby="security-label"> <div ngMenuItem value="Change password"> <span class="icon material-symbols-outlined">lock_open</span> <span class="label">Change password</span> </div> <div ngMenuItem value="Two-factor authentication"> <span class="icon material-symbols-outlined">security_key</span> <span class="label">Two-factor authentication</span> </div> <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()"> <span class="icon material-symbols-outlined">refresh</span> <span class="label">Reset</span> <span class="icon material-symbols-outlined">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu #updateMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Email address"> <span class="icon material-symbols-outlined">email</span> <span class="label">Email address</span> </div> <div ngMenuItem value="Phone number"> <span class="icon material-symbols-outlined">phone</span> <span class="label">Phone number</span> </div> <div ngMenuItem value="Password"> <span class="icon material-symbols-outlined">vpn_key</span> <span class="label">Password</span> </div> </ng-template> </div> </ng-template> </div> <div role="separator" class="separator"></div> <span id="help-label" class="heading">HELP</span> <div role="group" aria-labelledby="help-label"> <div ngMenuItem value="Support"> <span class="icon material-symbols-outlined">help</span> <span class="label">Support</span> </div> <div ngMenuItem value="Feedback"> <span class="icon material-symbols-outlined">feedback</span> <span class="label">Feedback</span> </div> </div> <div role="separator" class="separator"></div> <div ngMenuItem value="Logout"> <span class="icon material-symbols-outlined">logout</span> <span class="label">Logout</span> </div> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngMenu] { margin: 0; width: 30rem; padding: 0.25rem; border-radius: 0.5rem; border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenu] [ngMenu] { width: 15rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem; border-radius: 0.25rem;}[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuItem]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--border-color); margin: 0.25rem 0; opacity: 0.25;}[ngMenu] .heading { display: block; font-weight: bold; opacity: 0.6; font-size: 0.75rem; padding: 0.75rem; letter-spacing: 0.05rem;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, OverlayModule],})export class App { updateMenu = viewChild<Menu<string>>('updateMenu');}
app.html
<div ngMenu class="material-menu"> <ng-template ngMenuContent> <span id="security-label" class="heading">SECURITY</span> <div role="group" aria-labelledby="security-label"> <div ngMenuItem value="Change password"> <span class="icon material-symbols-outlined">lock_open</span> <span class="label">Change password</span> </div> <div ngMenuItem value="Two-factor authentication"> <span class="icon material-symbols-outlined">security_key</span> <span class="label">Two-factor authentication</span> </div> <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()"> <span class="icon material-symbols-outlined">refresh</span> <span class="label">Reset</span> <span class="icon material-symbols-outlined">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu #updateMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Email address"> <span class="icon material-symbols-outlined">email</span> <span class="label">Email address</span> </div> <div ngMenuItem value="Phone number"> <span class="icon material-symbols-outlined">phone</span> <span class="label">Phone number</span> </div> <div ngMenuItem value="Password"> <span class="icon material-symbols-outlined">vpn_key</span> <span class="label">Password</span> </div> </ng-template> </div> </ng-template> </div> <div role="separator" class="separator"></div> <span id="help-label" class="heading">HELP</span> <div role="group" aria-labelledby="help-label"> <div ngMenuItem value="Support"> <span class="icon material-symbols-outlined">help</span> <span class="label">Support</span> </div> <div ngMenuItem value="Feedback"> <span class="icon material-symbols-outlined">feedback</span> <span class="label">Feedback</span> </div> </div> <div role="separator" class="separator"></div> <div ngMenuItem value="Logout"> <span class="icon material-symbols-outlined">logout</span> <span class="label">Logout</span> </div> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}[ngMenu] { gap: 3px; width: 30rem; padding: 1rem; border-radius: 1rem; display: flex; flex-direction: column; background-color: color-mix(in srgb, var(--vivid-pink) 5%, var(--page-background)); border: 1px solid color-mix(in srgb, var(--full-contrast) 10%, var(--page-background));}[ngMenu] [ngMenu] { width: 15rem;}[ngMenu] .group { padding: 0.25rem; border-radius: 0.25rem; background-color: var(--page-background); box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);}[ngMenu] .group:first-of-type { border-top-left-radius: 1rem; border-top-right-radius: 1rem;}[ngMenu] .group:last-of-type { border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.75rem; font-size: 0.875rem; border-radius: 0.75rem;}[ngMenuItem][data-active='true'] { color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast)); background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);}[ngMenuItem]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--gray-500); margin: 0.25rem 0; opacity: 0.25;}[ngMenu] .heading { display: block; font-weight: bold; opacity: 0.6; font-size: 0.75rem; padding: 0.75rem; letter-spacing: 0.05rem;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, OverlayModule],})export class App { updateMenu = viewChild<Menu<string>>('updateMenu');}
app.html
<div ngMenu class="retro-menu"> <ng-template ngMenuContent> <span id="security-label" class="heading">SECURITY</span> <div role="group" aria-labelledby="security-label"> <div ngMenuItem value="Change password"> <span class="icon material-symbols-outlined">lock_open</span> <span class="label">Change password</span> </div> <div ngMenuItem value="Two-factor authentication"> <span class="icon material-symbols-outlined">security_key</span> <span class="label">Two-factor authentication</span> </div> <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()"> <span class="icon material-symbols-outlined">refresh</span> <span class="label">Reset</span> <span class="icon material-symbols-outlined">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu #updateMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Email address"> <span class="icon material-symbols-outlined">email</span> <span class="label">Email address</span> </div> <div ngMenuItem value="Phone number"> <span class="icon material-symbols-outlined">phone</span> <span class="label">Phone number</span> </div> <div ngMenuItem value="Password"> <span class="icon material-symbols-outlined">vpn_key</span> <span class="label">Password</span> </div> </ng-template> </div> </ng-template> </div> <div role="separator" class="separator"></div> <span id="help-label" class="heading">HELP</span> <div role="group" aria-labelledby="help-label"> <div ngMenuItem value="Support"> <span class="icon material-symbols-outlined">help</span> <span class="label">Support</span> </div> <div ngMenuItem value="Feedback"> <span class="icon material-symbols-outlined">feedback</span> <span class="label">Feedback</span> </div> </div> <div role="separator" class="separator"></div> <div ngMenuItem value="Logout"> <span class="icon material-symbols-outlined">logout</span> <span class="label">Logout</span> </div> </ng-template></div>
app.css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-size: 0.8rem; --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background)); font-family: 'Press Start 2P'; --retro-button-color: var(--vivid-pink); --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent); --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}[ngMenu] { margin-top: 8px; width: 30rem; padding: 0.25rem; background-color: var(--page-background); box-shadow: var(--retro-flat-shadow);}[ngMenu] [ngMenu] { width: 15rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem;}[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuItem]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: -4px;}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 4px solid #000; margin: 0.25rem 0; opacity: 0.25;}[ngMenu] .heading { display: block; font-weight: bold; opacity: 0.6; font-size: 0.75rem; padding: 0.75rem; letter-spacing: 0.05rem;}
Standalone menus work well for always-visible action lists or navigation.
Disabled menu items
Disable specific menu items using the disabled input. Control focus behavior with softDisabled.
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze" [disabled]="true"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" [disabled]="true" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font); --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; color: var(--primary-contrast); border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin: 0; width: 15rem; padding: 0.25rem; border-radius: 0.5rem; border: 1px solid var(--border-color); background-color: var(--page-background);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem; border-radius: 0.25rem;}[ngMenuItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngMenuTrigger]:hover,[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--border-color); margin: 0.25rem 0; opacity: 0.25;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as read"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div ngMenuItem value="Delete" [disabled]="true"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </div> <div class="group"> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div class="group"> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label" [disabled]="true"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </div> </ng-template> </div> </ng-template> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-family: var(--inter-font);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; border-radius: 0.5rem; border: 1px solid transparent; background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent); color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { gap: 3px; width: 15rem; display: flex; flex-direction: column;}[ngMenu] .group { padding: 0.25rem; border-radius: 0.25rem; background-color: var(--page-background); box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);}[ngMenu] .group:first-of-type { border-top-left-radius: 1rem; border-top-right-radius: 1rem;}[ngMenu] .group:last-of-type { border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem;}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.75rem; font-size: 0.875rem; border-radius: 0.75rem;}[ngMenuTrigger]:hover,[ngMenuTrigger][aria-expanded='true'] { background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);}[ngMenuItem][data-active='true'] { color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast)); background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);}[ngMenuItem]:focus,[ngMenuTrigger]:focus { outline: 2px solid var(--vivid-pink);}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 1px solid var(--gray-500); margin: 0.25rem 0; opacity: 0.25;}[ngMenuItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}
app.ts
import {Component, viewChild} from '@angular/core';import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';import {OverlayModule} from '@angular/cdk/overlay';@Component({ selector: 'app-root', templateUrl: 'app.html', styleUrl: 'app.css', imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],})export class App { formatMenu = viewChild<Menu<string>>('formatMenu'); categorizeMenu = viewChild<Menu<string>>('categorizeMenu');}
app.html
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()"> Open Menu</button><ng-template [cdkConnectedOverlayOpen]="trigger.expanded()" [cdkConnectedOverlay]="{origin, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}]" cdkAttachPopoverAsChild> <div ngMenu class="menu" #formatMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as read" [disabled]="true"> <span class="icon material-symbols-outlined">mark_email_read</span> <span class="label">Mark as read</span> </div> <div ngMenuItem value="Snooze"> <span class="icon material-symbols-outlined">snooze</span> <span class="label">Snooze</span> </div> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem class="menu-item" value="Categorize" #categorizeItem [submenu]="categorizeMenu()" > <span class="icon material-symbols-outlined">category</span> <span class="label">Categorize</span> <span class="icon material-symbols-outlined arrow">arrow_right</span> </div> <ng-template [cdkConnectedOverlayOpen]="formatMenu.visible()" [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}" [cdkConnectedOverlayPositions]="[{originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6}]" cdkAttachPopoverAsChild > <div ngMenu class="menu" #categorizeMenu="ngMenu"> <ng-template ngMenuContent> <div ngMenuItem value="Mark as important"> <span class="icon material-symbols-outlined">label_important</span> <span class="label">Mark as important</span> </div> <div ngMenuItem value="Star"> <span class="icon material-symbols-outlined">star</span> <span class="label">Star</span> </div> <div ngMenuItem value="Label"> <span class="icon material-symbols-outlined">label</span> <span class="label">Label</span> </div> </ng-template> </div> </ng-template> <div role="separator" aria-orientation="horizontal" class="separator"></div> <div ngMenuItem value="Archive"> <span class="icon material-symbols-outlined">archive</span> <span class="label">Archive</span> </div> <div ngMenuItem value="Report spam"> <span class="icon material-symbols-outlined">report</span> <span class="label">Report spam</span> </div> <div ngMenuItem value="Delete"> <span class="icon material-symbols-outlined">delete</span> <span class="label">Delete</span> </div> </ng-template> </div></ng-template>
app.css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; font-size: 0.8rem; --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background)); font-family: 'Press Start 2P'; --retro-button-color: var(--vivid-pink); --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent); --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent); --retro-elevated-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-flat-shadow: 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700); --retro-clickable-shadow: inset 4px 4px 0px 0px var(--retro-shadow-light), inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 8px 8px 0px 0px var(--gray-700); --retro-pressed-shadow: inset 4px 4px 0px 0px var(--retro-shadow-dark), inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--gray-700), 0px 4px 0px 0px var(--gray-700), -4px 0px 0px 0px var(--gray-700), 0px -4px 0px 0px var(--gray-700), 0px 0px 0px 0px var(--gray-700);}[ngMenuTrigger] { display: flex; cursor: pointer; align-items: center; padding: 0.6rem 2rem; font-family: 'Press Start 2P'; color: #000; background-color: var(--vivid-pink); box-shadow: var(--retro-clickable-shadow); transition: transform 0.1s, box-shadow 0.1s;}[ngMenuTrigger]:hover { transform: translate(1px, 1px);}[ngMenuTrigger]:active { background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff); box-shadow: var(--retro-pressed-shadow); transform: translate(4px, 4px);}[ngMenuTrigger] .icon { font-size: 1.5rem; opacity: 0.875;}[ngMenu] { margin-top: 8px; width: 15rem; padding: 0.25rem; background-color: var(--page-background); box-shadow: var(--retro-flat-shadow);}[ngMenu][data-visible='false'] { display: none;}[ngMenuItem] { outline: none; display: flex; cursor: pointer; align-items: center; gap: 0.5rem; padding: 0.5rem; font-size: 0.875rem;}[ngMenuItem][data-active='true'] { background: color-mix(in srgb, var(--border-color) 10%, transparent);}[ngMenuTrigger]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: 8px;}[ngMenuItem]:focus { outline: 4px dashed var(--vivid-pink); outline-offset: -4px;}[ngMenuItem] .icon { opacity: 0.875; font-size: 1.25rem;}[ngMenuItem] .label { flex: 1; opacity: 0.875; font-size: 0.875rem;}[ngMenuItem]:not([aria-expanded='true']) .arrow { opacity: 0.5;}[ngMenu] .separator { border-top: 4px solid #000; margin: 0.25rem 0; opacity: 0.25;}[ngMenuItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}
When [softDisabled]="true", disabled items can receive focus but cannot be activated. When [softDisabled]="false", disabled items are skipped during keyboard navigation.
APIs
Menu
The container directive for menu items.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
disabled |
boolean |
false |
Disables all items in the menu |
wrap |
boolean |
true |
Whether keyboard navigation wraps at edges |
softDisabled |
boolean |
true |
When true, disabled items are focusable but not interactive |
Methods
| Method | Parameters | Description |
|---|---|---|
close |
none | Closes the menu |
focusFirstItem |
none | Moves focus to the first menu item |
MenuBar
A horizontal container for multiple menus.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
disabled |
boolean |
false |
Disables the entire menubar |
wrap |
boolean |
true |
Whether keyboard navigation wraps at edges |
softDisabled |
boolean |
true |
When true, disabled items are focusable but not interactive |
MenuItem
An individual item within a menu.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
value |
any |
— | Required. Value for this item |
disabled |
boolean |
false |
Disables this menu item |
submenu |
Menu |
— | Reference to a submenu |
searchTerm |
string |
'' |
Search term for typeahead (supports two-way binding) |
Signals
| Property | Type | Description |
|---|---|---|
active |
Signal<boolean> |
Whether the item currently has focus |
expanded |
Signal<boolean> |
Whether the submenu is expanded |
hasPopup |
Signal<boolean> |
Whether the item has an associated submenu |
NOTE: MenuItem does not expose public methods. Use the submenu input to associate submenus with menu items.
MenuTrigger
A button or element that opens a menu.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
menu |
Menu |
— | Required. The menu to trigger |
disabled |
boolean |
false |
Disables the trigger |
softDisabled |
boolean |
true |
When true, disabled trigger is focusable |
Signals
| Property | Type | Description |
|---|---|---|
expanded |
Signal<boolean> |
Whether the menu is currently open |
hasPopup |
Signal<boolean> |
Whether the trigger has an associated menu |
Methods
| Method | Parameters | Description |
|---|---|---|
open |
none | Opens the menu |
close |
none | Closes the menu |
toggle |
none | Toggles the menu open/closed |