Spec-Zone.ru › Angular

Menu

Menu ARIA pattern Menu API Reference

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.

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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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;
}
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');
}
<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" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}
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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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.

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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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;
}
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');
}
<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" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}
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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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.

import {Component, viewChild, signal} 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 {
  trigger = viewChild.required<MenuTrigger<string>>('trigger');
  contextMenu = viewChild<Menu<string>>('contextMenu');

  // Position of the context menu
  menuPosition = signal({x: 0, y: 0});

  onContextMenu(event: MouseEvent) {
    event.preventDefault();

    // Update position coordinates
    this.menuPosition.set({x: event.clientX, y: event.clientY});

    // Open the menu via the trigger
    this.trigger().open();
  }

  onItemSelected(value: string) {
    console.log(`Action selected: ${value}`);
  }
}
<!-- Target area for right-click -->
<div (contextmenu)="onContextMenu($event)" class="context-zone">
  Right-click inside this area to open the context menu.
</div>

<!-- Hidden trigger positioned dynamically at cursor coordinates -->
<div
  #triggerEl
  ngMenuTrigger
  #trigger="ngMenuTrigger"
  [menu]="contextMenu()"
  style="position: fixed; visibility: hidden"
  [style.left.px]="menuPosition().x"
  [style.top.px]="menuPosition().y"
></div>

<!-- Overlay container for the menu -->
<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin: triggerEl, usePopover: 'inline'}"
  cdkAttachPopoverAsChild
>
  <div ngMenu #contextMenu="ngMenu" (itemSelected)="onItemSelected($event)">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Edit">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">edit</span>
        <span class="label">Edit</span>
      </div>

      <div ngMenuItem value="Copy">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >content_copy</span
        >
        <span class="label">Copy</span>
      </div>

      <div role="separator" class="separator"></div>

      <div ngMenuItem value="Delete" style="color: var(--vivid-pink)">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>

Position the menu using the contextmenu event coordinates.

Standalone menu

A standalone menu doesn't require a trigger and remains visible in the interface.

import {Component, signal, 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 {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div ngMenu (mouseover)="hasInteracted.set(true)" (focusin)="hasInteracted.set(true)">
  <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" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [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" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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;
}
import {Component, signal, 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 {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div
  ngMenu
  class="material-menu"
  (mouseover)="hasInteracted.set(true)"
  (focusin)="hasInteracted.set(true)"
>
  <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" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [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" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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(--quaternary-contrast);
  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;
}
import {Component, signal, 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 {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div
  ngMenu
  class="retro-menu"
  (mouseover)="hasInteracted.set(true)"
  (focusin)="hasInteracted.set(true)"
>
  <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" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [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" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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.

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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze" [disabled]="true">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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;
}
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');
}
<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" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete" [disabled]="true">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >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" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label" [disabled]="true">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenuItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}
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');
}
<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" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">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" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >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" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >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" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --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(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[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]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='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.

Testing

Angular Aria provides component harnesses for testing menu components. Here is an example of how to use the harnesses in a component test:

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MenuHarness, MenuItemHarness} from '@angular/aria/menu/testing';
import {MyMenuComponent} from './my-menu'; // Your component

describe('MyMenuComponent', () => {
  let fixture: ComponentFixture<MyMenuComponent>;
  let loader: HarnessLoader;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [MyMenuComponent],
    });

    fixture = TestBed.createComponent(MyMenuComponent);
    await fixture.whenStable();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should open menu and click item', async () => {
    // Load the menu harness by its trigger text
    const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));

    // Verify initial state
    expect(await menu.isOpen()).toBe(false);

    // Open the menu
    await menu.open();
    expect(await menu.isOpen()).toBe(true);

    // Get items
    const items = await menu.getItems();
    expect(items.length).toBe(3);
    expect(await items[0].getText()).toBe('Item 1');

    // Click first item
    await items[0].click();

    // Menu should close after selection (depending on your implementation)
    expect(await menu.isOpen()).toBe(false);
  });

  it('should interact with submenus', async () => {
    const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
    await menu.open();

    // Get the item that triggers a submenu
    const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'}));
    expect(await subItem.hasSubmenu()).toBe(true);

    // Open submenu
    await subItem.click();
    const submenu = await subItem.getSubmenu();
    expect(submenu).toBeTruthy();
    expect(await submenu!.isOpen()).toBe(true);

    // Interact with submenu items
    const subItems = await submenu!.getItems();
    expect(subItems.length).toBe(1);
  });
});

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

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
role 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' 'menuitem' The ARIA role for the menu 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

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/aria/menu

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API