Skip to content
Ndop UI

Dropdown Menu

A menu of actions or options that a button opens, such as a row's actions in a table.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from '@/ui/dropdown-menu/dropdown-menu';

/**
 * An account menu: labelled groups, shortcuts, a submenu, a disabled item
 * and a destructive action, reporting what was chosen.
 */
@Component({
  selector: 'example-dropdown-menu-demo',
  imports: [
    Button,
    DropdownMenuContent,
    DropdownMenuGroup,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuSeparator,
    DropdownMenuShortcut,
    DropdownMenuSubContent,
    DropdownMenuSubTrigger,
    DropdownMenuTrigger,
    NdIcon,
  ],
  template: `
    <div class="flex flex-col items-start gap-3">
      <button ndButton variant="outline" [ndDropdownMenuTrigger]="account">
        Open
      </button>
      <p class="text-sm text-muted-foreground" aria-live="polite">
        {{ chosen() }}
      </p>
    </div>
    <ng-template #account>
      <div ndDropdownMenuContent class="w-56">
        <div ndDropdownMenuGroup aria-labelledby="dropdown-menu-demo-account">
          <div ndDropdownMenuLabel id="dropdown-menu-demo-account">
            My account
          </div>
          <div ndDropdownMenuItem (triggered)="choose('Profile')">
            Profile <span ndDropdownMenuShortcut>⇧⌘P</span>
          </div>
          <div ndDropdownMenuItem (triggered)="choose('Billing')">
            Billing <span ndDropdownMenuShortcut>⌘B</span>
          </div>
          <div ndDropdownMenuItem (triggered)="choose('Settings')">
            Settings <span ndDropdownMenuShortcut>⌘S</span>
          </div>
        </div>
        <div ndDropdownMenuSeparator></div>
        <div ndDropdownMenuGroup aria-label="Team">
          <div ndDropdownMenuItem (triggered)="choose('Team')">Team</div>
          <div [ndDropdownMenuSubTrigger]="invite">
            Invite users
            <svg ndIcon="chevron-right" class="ms-auto rtl:rotate-180"></svg>
          </div>
          <div ndDropdownMenuItem disabled>API (coming soon)</div>
        </div>
        <div ndDropdownMenuSeparator></div>
        <div
          ndDropdownMenuItem
          variant="destructive"
          (triggered)="choose('Log out')"
        >
          Log out <span ndDropdownMenuShortcut>⇧⌘Q</span>
        </div>
      </div>
    </ng-template>
    <ng-template #invite>
      <div ndDropdownMenuSubContent>
        <div ndDropdownMenuItem (triggered)="choose('Invite by email')">
          Email
        </div>
        <div ndDropdownMenuItem (triggered)="choose('Invite by message')">
          Message
        </div>
      </div>
    </ng-template>
  `,
})
export class DropdownMenuDemoExample {
  protected readonly chosen = signal('');

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

A dropdown menu lists actions or options under a button, such as a row's actions in a table or an account menu. It's a menu button: Enter, Space or the arrow keys open it on the first or last item, the arrow keys and typing move between items, and Escape closes it and returns to the button. Items can be checkboxes, radios or open submenus, and can be disabled while staying reachable.

Installation

npx @ndop/cli add dropdown-menu

Usage

import { Button } from '@/ui/button/button';
import {
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from '@/ui/dropdown-menu/dropdown-menu';
<div class="flex flex-col items-start gap-3">
  <button ndButton variant="outline" [ndDropdownMenuTrigger]="account">
    Open
  </button>
  <p class="text-sm text-muted-foreground" aria-live="polite">
    {{ chosen() }}
  </p>
</div>
<ng-template #account>
  <div ndDropdownMenuContent class="w-56">
    <div ndDropdownMenuGroup aria-labelledby="dropdown-menu-demo-account">
      <div ndDropdownMenuLabel id="dropdown-menu-demo-account">
        My account
      </div>
      <div ndDropdownMenuItem (triggered)="choose('Profile')">
        Profile <span ndDropdownMenuShortcut>⇧⌘P</span>
      </div>
      <div ndDropdownMenuItem (triggered)="choose('Billing')">
        Billing <span ndDropdownMenuShortcut>⌘B</span>
      </div>
      <div ndDropdownMenuItem (triggered)="choose('Settings')">
        Settings <span ndDropdownMenuShortcut>⌘S</span>
      </div>
    </div>
    <div ndDropdownMenuSeparator></div>
    <div ndDropdownMenuGroup aria-label="Team">
      <div ndDropdownMenuItem (triggered)="choose('Team')">Team</div>
      <div [ndDropdownMenuSubTrigger]="invite">
        Invite users
        <svg ndIcon="chevron-right" class="ms-auto rtl:rotate-180"></svg>
      </div>
      <div ndDropdownMenuItem disabled>API (coming soon)</div>
    </div>
    <div ndDropdownMenuSeparator></div>
    <div
      ndDropdownMenuItem
      variant="destructive"
      (triggered)="choose('Log out')"
    >
      Log out <span ndDropdownMenuShortcut>⇧⌘Q</span>
    </div>
  </div>
</ng-template>
<ng-template #invite>
  <div ndDropdownMenuSubContent>
    <div ndDropdownMenuItem (triggered)="choose('Invite by email')">
      Email
    </div>
    <div ndDropdownMenuItem (triggered)="choose('Invite by message')">
      Message
    </div>
  </div>
</ng-template>

Examples

Checkbox items that show or hide panels, keeping the menu open on Space.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItemIndicator,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from '@/ui/dropdown-menu/dropdown-menu';

/**
 * Checkbox items that show or hide panels. Space toggles one and keeps the
 * menu open; Enter or a click toggles it and closes the menu.
 */
@Component({
  selector: 'example-dropdown-menu-checkboxes',
  imports: [
    Button,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuGroup,
    DropdownMenuItemIndicator,
    DropdownMenuLabel,
    DropdownMenuTrigger,
    NdIcon,
  ],
  template: `
    <button ndButton variant="outline" [ndDropdownMenuTrigger]="panels">
      Appearance
    </button>
    <ng-template #panels>
      <div ndDropdownMenuContent class="w-56">
        <div
          ndDropdownMenuGroup
          aria-labelledby="dropdown-menu-checkboxes-label"
        >
          <div ndDropdownMenuLabel id="dropdown-menu-checkboxes-label">
            Panels
          </div>
          <div ndDropdownMenuCheckboxItem [(checked)]="statusBar">
            <span ndDropdownMenuItemIndicator><svg ndIcon="check"></svg></span>
            Status bar
          </div>
          <div ndDropdownMenuCheckboxItem [(checked)]="activityBar" disabled>
            <span ndDropdownMenuItemIndicator><svg ndIcon="check"></svg></span>
            Activity bar
          </div>
          <div ndDropdownMenuCheckboxItem [(checked)]="panel">
            <span ndDropdownMenuItemIndicator><svg ndIcon="check"></svg></span>
            Panel
          </div>
        </div>
      </div>
    </ng-template>
  `,
})
export class DropdownMenuCheckboxesExample {
  protected readonly statusBar = signal(true);
  protected readonly activityBar = signal(false);
  protected readonly panel = signal(false);
}

Radio items that choose one panel position.

import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import {
  DropdownMenuContent,
  DropdownMenuItemIndicator,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/ui/dropdown-menu/dropdown-menu';

/** Radio items that choose one panel position. */
@Component({
  selector: 'example-dropdown-menu-radio-group',
  imports: [
    Button,
    DropdownMenuContent,
    DropdownMenuItemIndicator,
    DropdownMenuLabel,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
    DropdownMenuSeparator,
    DropdownMenuTrigger,
    NdIcon,
  ],
  template: `
    <button ndButton variant="outline" [ndDropdownMenuTrigger]="positions">
      Panel: {{ position() }}
    </button>
    <ng-template #positions>
      <div ndDropdownMenuContent class="w-56">
        <div ndDropdownMenuLabel id="dropdown-menu-radio-group-label">
          Panel position
        </div>
        <div ndDropdownMenuSeparator></div>
        <div
          ndDropdownMenuRadioGroup
          aria-labelledby="dropdown-menu-radio-group-label"
          [(value)]="position"
        >
          @for (option of options; track option) {
            <div ndDropdownMenuRadioItem [value]="option" class="capitalize">
              <span ndDropdownMenuItemIndicator>
                <svg ndIcon="circle" class="size-2 fill-current"></svg>
              </span>
              {{ option }}
            </div>
          }
        </div>
      </div>
    </ng-template>
  `,
})
export class DropdownMenuRadioGroupExample {
  protected readonly options = ['top', 'bottom', 'end'];
  protected readonly position = signal<string | null>('bottom');
}

Accessibility

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

Keyboard

Keys the component handles.

Key Action
EnterorSpaceOpens the menu and focuses its first item.
ArrowDownOpens the menu and focuses its first item.
ArrowUpOpens the menu and focuses its last 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

  • Closing the menu returns focus to the trigger.
  • 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

start and end sides are logical and swap in right-to-left layouts. 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

DropdownMenuTrigger

[ndDropdownMenuTrigger]

The button that opens a dropdown menu (@ndop/primitives/dropdown-menu): its value is the menu's ng-template. Enter, Space and the arrow keys open it on an item; closing returns focus here.

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

DropdownMenuTrigger binding Type Default
[ndDropdownMenuTrigger]
The ng-template of the menu it opens.
TemplateRef<unknown> | nullnull
[defaultOpen]
Initial open state when open isn't bound, applied after hydration.
booleanfalse
[side]
Side of the trigger the menu opens on, before flipping to fit.
'bottom' | 'top' | 'start' | 'end''bottom'
[align]
Alignment of the menu along that side.
'start' | 'end' | 'center''start'
[sideOffset]
Gap between the trigger and the menu, in pixels.
number4
[(open)]
Whether the menu is open. Two-way bindable with [(open)].
booleanfalse
Data attribute Values
data-slotdropdown-menu-trigger
data-state —

DropdownMenuContent

[ndDropdownMenuContent]role menu

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

Manages aria-orientation.

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

DropdownMenuSubContent

[ndDropdownMenuSubContent]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.

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

DropdownMenuGroup

[ndDropdownMenuGroup]role group

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

Data attribute Values
data-slotdropdown-menu-group

DropdownMenuItem

[ndDropdownMenuItem]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.

DropdownMenuItem 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-slotdropdown-menu-item
data-inset —
data-variant —
data-disabled —

DropdownMenuCheckboxItem

[ndDropdownMenuCheckboxItem]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.

DropdownMenuCheckboxItem 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-slotdropdown-menu-checkbox-item
data-state —
data-disabled —

DropdownMenuRadioGroup

[ndDropdownMenuRadioGroup]role group

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

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

DropdownMenuRadioItem

[ndDropdownMenuRadioItem]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.

DropdownMenuRadioItem 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-slotdropdown-menu-radio-item
data-state —
data-disabled —

DropdownMenuItemIndicator

[ndDropdownMenuItemIndicator]

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

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

DropdownMenuLabel

[ndDropdownMenuLabel]

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

DropdownMenuLabel 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-slotdropdown-menu-label
data-inset —

DropdownMenuSeparator

[ndDropdownMenuSeparator]role separator

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

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

DropdownMenuShortcut

[ndDropdownMenuShortcut]

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

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

DropdownMenuSubTrigger

[ndDropdownMenuSubTrigger]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.

DropdownMenuSubTrigger 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''
[ndDropdownMenuSubTrigger]
The ng-template of the submenu.
TemplateRef<unknown> | nullnull
[disabled]
Makes it inert but focusable, with aria-disabled.
booleanfalse
Data attribute Values
data-slotdropdown-menu-sub-trigger
data-inset —
data-state —
data-disabled —

Agent notes

Do

  • Name the button after what the menu is for, such as "Actions" or an icon with an aria-label.
  • Group related items, with labels and separators.
  • Mark dangerous actions with variant="destructive".

Don't

  • Don't put links to pages in it; use Navigation Menu.
  • Don't use it for choosing one value in a form; use a Select.