Skip to content
Ndop UI

Context Menu

A menu of actions that a right click or long press on an area opens, at the pointer.

  • v1.0.0
  • WAI-ARIA: Menu
  • SSR
  • Zoneless
  • 3.0 kB gzip
  • +20.3 kB Angular CDK, shared
Right click here
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuItemIndicator,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from '@/ui/context-menu/context-menu';

/**
 * Browser-style actions on an area: shortcuts, a disabled item, a submenu,
 * a checkbox and a group of radios. The area is focusable and named, so
 * Shift + F10 opens the menu from the keyboard.
 */
@Component({
  selector: 'example-context-menu-demo',
  imports: [
    ContextMenuCheckboxItem,
    ContextMenuContent,
    ContextMenuItem,
    ContextMenuItemIndicator,
    ContextMenuLabel,
    ContextMenuRadioGroup,
    ContextMenuRadioItem,
    ContextMenuSeparator,
    ContextMenuShortcut,
    ContextMenuSubContent,
    ContextMenuSubTrigger,
    ContextMenuTrigger,
    NdIcon,
  ],
  template: `
    <div
      [ndContextMenuTrigger]="menu"
      tabindex="0"
      aria-label="Page preview"
      class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
    >
      Right click here
    </div>
    <ng-template #menu>
      <div ndContextMenuContent class="w-52">
        <div ndContextMenuItem inset>
          Back <span ndContextMenuShortcut>⌘[</span>
        </div>
        <div ndContextMenuItem inset disabled>
          Forward <span ndContextMenuShortcut>⌘]</span>
        </div>
        <div ndContextMenuItem inset>
          Reload <span ndContextMenuShortcut>⌘R</span>
        </div>
        <div [ndContextMenuSubTrigger]="tools" inset>
          More tools
          <svg ndIcon="chevron-right" class="ms-auto rtl:rotate-180"></svg>
        </div>
        <div ndContextMenuSeparator></div>
        <div ndContextMenuCheckboxItem [(checked)]="bookmarks">
          <span ndContextMenuItemIndicator><svg ndIcon="check"></svg></span>
          Show bookmarks
        </div>
        <div ndContextMenuSeparator></div>
        <div ndContextMenuLabel inset id="context-menu-demo-people">People</div>
        <div
          ndContextMenuRadioGroup
          aria-labelledby="context-menu-demo-people"
          [(value)]="person"
        >
          @for (name of people; track name) {
            <div ndContextMenuRadioItem [value]="name">
              <span ndContextMenuItemIndicator>
                <svg ndIcon="circle" class="size-2 fill-current"></svg>
              </span>
              {{ name }}
            </div>
          }
        </div>
      </div>
    </ng-template>
    <ng-template #tools>
      <div ndContextMenuSubContent class="w-44">
        <div ndContextMenuItem>Save page…</div>
        <div ndContextMenuItem>Developer tools</div>
        <div ndContextMenuSeparator></div>
        <div ndContextMenuItem variant="destructive">Delete</div>
      </div>
    </ng-template>
  `,
})
export class ContextMenuDemoExample {
  protected readonly bookmarks = signal(true);
  protected readonly people = ['Pedro Duarte', 'Colm Tuite'];
  protected readonly person = signal<string | null>('Pedro Duarte');
}

A context menu lists actions for an area, opened at the pointer with a right click or a long press, or at the area's corner with Shift + F10 or the keyboard's menu key. Inside, it works like any menu: arrow keys, typing, Enter and Escape, which returns focus to the area. Items can be checkboxes, radios or submenus.

Installation

npx @ndop/cli add context-menu

Usage

import {
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuItemIndicator,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from '@/ui/context-menu/context-menu';
<div
  [ndContextMenuTrigger]="menu"
  tabindex="0"
  aria-label="Page preview"
  class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
  Right click here
</div>
<ng-template #menu>
  <div ndContextMenuContent class="w-52">
    <div ndContextMenuItem inset>
      Back <span ndContextMenuShortcut>⌘[</span>
    </div>
    <div ndContextMenuItem inset disabled>
      Forward <span ndContextMenuShortcut>⌘]</span>
    </div>
    <div ndContextMenuItem inset>
      Reload <span ndContextMenuShortcut>⌘R</span>
    </div>
    <div [ndContextMenuSubTrigger]="tools" inset>
      More tools
      <svg ndIcon="chevron-right" class="ms-auto rtl:rotate-180"></svg>
    </div>
    <div ndContextMenuSeparator></div>
    <div ndContextMenuCheckboxItem [(checked)]="bookmarks">
      <span ndContextMenuItemIndicator><svg ndIcon="check"></svg></span>
      Show bookmarks
    </div>
    <div ndContextMenuSeparator></div>
    <div ndContextMenuLabel inset id="context-menu-demo-people">People</div>
    <div
      ndContextMenuRadioGroup
      aria-labelledby="context-menu-demo-people"
      [(value)]="person"
    >
      @for (name of people; track name) {
        <div ndContextMenuRadioItem [value]="name">
          <span ndContextMenuItemIndicator>
            <svg ndIcon="circle" class="size-2 fill-current"></svg>
          </span>
          {{ name }}
        </div>
      }
    </div>
  </div>
</ng-template>
<ng-template #tools>
  <div ndContextMenuSubContent class="w-44">
    <div ndContextMenuItem>Save page…</div>
    <div ndContextMenuItem>Developer tools</div>
    <div ndContextMenuSeparator></div>
    <div ndContextMenuItem variant="destructive">Delete</div>
  </div>
</ng-template>

Examples

Context Menu checkboxes

Checkbox items on a right click, for showing and hiding layers.

Map Traffic
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItemIndicator,
  ContextMenuTrigger,
} from '@/ui/context-menu/context-menu';

/**
 * Checkbox items on a right click, for showing and hiding a map's
 * layers, which the area reflects.
 */
@Component({
  selector: 'example-context-menu-checkboxes',
  imports: [
    ContextMenuCheckboxItem,
    ContextMenuContent,
    ContextMenuItemIndicator,
    ContextMenuTrigger,
    NdIcon,
  ],
  template: `
    <div
      [ndContextMenuTrigger]="layers"
      tabindex="0"
      aria-label="Map"
      class="grid h-[150px] w-[300px] place-content-center gap-1 rounded-md border text-center text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
    >
      <span>Map</span>
      <span class="text-muted-foreground">
        {{ traffic() ? 'Traffic' : '' }} {{ transit() ? 'Transit' : '' }}
      </span>
    </div>
    <ng-template #layers>
      <div ndContextMenuContent class="w-44">
        <div ndContextMenuCheckboxItem [(checked)]="traffic">
          <span ndContextMenuItemIndicator><svg ndIcon="check"></svg></span>
          Traffic
        </div>
        <div ndContextMenuCheckboxItem [(checked)]="transit">
          <span ndContextMenuItemIndicator><svg ndIcon="check"></svg></span>
          Transit
        </div>
      </div>
    </ng-template>
  `,
})
export class ContextMenuCheckboxesExample {
  protected readonly traffic = signal(true);
  protected readonly transit = signal(false);
}

Accessibility

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

Keyboard

Keys the component handles.

Key Action
ShiftF10On the focused area, opens the menu and focuses its first item.
ArrowDownorArrowUpMoves focus to the next or previous item, wrapping.
HomeorEndMoves focus to the first or last item.
EnterActivates the focused item and closes the menu.
SpaceActivates the focused item; checkbox and radio items keep the menu open.
ArrowRightOn a sub-trigger, opens its menu and focuses the first item.
ArrowLeftIn a submenu, closes it and focuses its sub-trigger.
EscapeCloses the current menu and returns focus to what opened it.
TabCloses every open menu and moves focus on from the trigger.
aTypeahead: moves focus to the next item whose label starts with the typed letters.

Focus

  • Opening focuses the menu; closing returns focus to the area.
  • Opened from the keyboard, a menu focuses its first item (its last with ArrowUp on a Dropdown Menu trigger); opened with the pointer, the menu itself.
  • Closing returns focus to the trigger or sub-trigger.

Right-to-left

Submenus follow Menu's RTL behavior. ArrowLeft opens submenus and ArrowRight closes them, and submenus open on the left. Direction comes from the app's Directionality: the document's dir, or a CDK [dir] directive.

API reference

ContextMenuTrigger

[ndContextMenuTrigger]

An area with a context menu (@ndop/primitives/context-menu): its value is the menu's ng-template, opened at the pointer on a right click or long press, or at its corner on Shift + F10. Make it focusable and named.

ContextMenuTrigger binding Type Default
[ndContextMenuTrigger]
The ng-template of the menu it opens.
TemplateRef<unknown> | nullnull
[disabled]
Leaves the browser's own context menu.
booleanfalse
Data attribute Values
data-slotcontext-menu-trigger
data-state —
data-disabled —

ContextMenuContent

[ndContextMenuContent]role menu

A context menu's list (@ndop/primitives/menu), in the ng-template a [ndContextMenuTrigger] area opens at the pointer on a right click or long press, or at its corner on Shift + F10.

Manages aria-orientation.

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

ContextMenuSubContent

[ndContextMenuSubContent]role menu

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

Manages aria-orientation.

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

ContextMenuGroup

[ndContextMenuGroup]role group

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

Data attribute Values
data-slotcontext-menu-group

ContextMenuItem

[ndContextMenuItem]role menuitem

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

Manages aria-disabled.

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

ContextMenuCheckboxItem

[ndContextMenuCheckboxItem]role menuitemcheckbox

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

Manages aria-checked, aria-disabled.

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

ContextMenuRadioGroup

[ndContextMenuRadioGroup]role group

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

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

ContextMenuRadioItem

[ndContextMenuRadioItem]role menuitemradio

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

Manages aria-checked, aria-disabled.

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

ContextMenuItemIndicator

[ndContextMenuItemIndicator]

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

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

ContextMenuLabel

[ndContextMenuLabel]

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

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

ContextMenuSeparator

[ndContextMenuSeparator]role separator

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

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

ContextMenuShortcut

[ndContextMenuShortcut]

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

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

ContextMenuSubTrigger

[ndContextMenuSubTrigger]role menuitem

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

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

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

Agent notes

Do

  • Make the area focusable and give it a name, so keyboard users can open the menu.
  • Offer the same actions somewhere visible too, such as a toolbar or a Dropdown Menu.

Don't

  • Don't hide actions people need only in a context menu.
  • Don't replace the browser's menu on text people may want to copy.