Dropdown Menu
A menu of actions or options that a button opens, such as a row's actions in a table.
- v1.0.0
- WAI-ARIA: Menu Button
- SSR
- Zoneless
- 3.5 kB gzip
- +19.8 kB Angular CDK, shared
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-menupnpm dlx @ndop/cli add dropdown-menuyarn dlx @ndop/cli add dropdown-menubunx @ndop/cli add dropdown-menuInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/dropdown-menu/dropdown-menu.tsimport { booleanAttribute, computed, Directive, input } from '@angular/core'; import { NdDropdownMenuTrigger } from '@ndop/primitives/dropdown-menu'; import { NdMenuContent, NdMenuGroup, NdMenuItem, NdMenuItemCheckbox, NdMenuItemIndicator, NdMenuItemRadio, NdMenuLabel, NdMenuRadioGroup, NdMenuSeparator, NdMenuSubTrigger, } from '@ndop/primitives/menu'; import { cn } from '@/lib/utils'; /** * 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. * * @example * ```html * <button ndButton [ndDropdownMenuTrigger]="menu">Open</button> * ``` */ @Directive({ selector: '[ndDropdownMenuTrigger]', hostDirectives: [ { directive: NdDropdownMenuTrigger, inputs: [ 'ndDropdownMenuTrigger', 'open', 'defaultOpen', 'side', 'align', 'sideOffset', ], outputs: ['openChange'], }, ], host: { 'data-slot': 'dropdown-menu-trigger' }, }) export class DropdownMenuTrigger {} /** * 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. * * @example * ```html * <div ndDropdownMenuContent><div ndDropdownMenuItem>Profile</div></div> * ``` */ @Directive({ selector: '[ndDropdownMenuContent]', hostDirectives: [{ directive: NdMenuContent, outputs: ['closed'] }], host: { 'data-slot': 'dropdown-menu-content', '[class]': 'classes()' }, }) export class DropdownMenuContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'z-50 max-h-[80vh] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', this.class(), ), ); } /** * A submenu, opened beside its sub-trigger: at the end side, so on the left * in right-to-left layouts. * * @example * ```html * <div ndDropdownMenuSubContent>…</div> * ``` */ @Directive({ selector: '[ndDropdownMenuSubContent]', hostDirectives: [{ directive: NdMenuContent, outputs: ['closed'] }], host: { 'data-slot': 'dropdown-menu-sub-content', '[class]': 'classes()' }, }) export class DropdownMenuSubContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', this.class(), ), ); } /** * Groups related items (`role="group"`). Name it with `aria-labelledby` * pointing at a label. * * @example * ```html * <div ndDropdownMenuGroup aria-labelledby="account">…</div> * ``` */ @Directive({ selector: '[ndDropdownMenuGroup]', hostDirectives: [NdMenuGroup], host: { 'data-slot': 'dropdown-menu-group' }, }) export class DropdownMenuGroup {} /** * One action (`role="menuitem"`): activating it emits `triggered` and * closes the menu. Disabled items stay focusable and do nothing. Use a * `div`: on a `button`, Space would close the menu. * * @example * ```html * <div ndDropdownMenuItem (triggered)="edit()">Edit</div> * ``` */ @Directive({ selector: '[ndDropdownMenuItem]', hostDirectives: [ { directive: NdMenuItem, inputs: ['disabled'], outputs: ['triggered'] }, ], host: { 'data-slot': 'dropdown-menu-item', '[attr.data-inset]': 'inset() ? "" : null', '[attr.data-variant]': 'variant()', '[class]': 'classes()', }, }) export class DropdownMenuItem { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** `destructive` colors a dangerous action, such as Delete. */ readonly variant = input<'default' | 'destructive'>('default'); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", this.class(), ), ); } /** * An item that is on or off (`role="menuitemcheckbox"`), bound with * `[(checked)]`. Put an indicator with a check icon in it. * * @example * ```html * <div ndDropdownMenuCheckboxItem [(checked)]="statusBar">…</div> * ``` */ @Directive({ selector: '[ndDropdownMenuCheckboxItem]', hostDirectives: [ { directive: NdMenuItemCheckbox, inputs: ['checked', 'disabled'], outputs: ['checkedChange', 'triggered'], }, ], host: { 'data-slot': 'dropdown-menu-checkbox-item', '[class]': 'classes()' }, }) export class DropdownMenuCheckboxItem { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", this.class(), ), ); } /** * Groups radio items (`role="group"`) and holds the checked value, bound * with `[(value)]`. * * @example * ```html * <div ndDropdownMenuRadioGroup aria-label="Panel position" [(value)]="position">…</div> * ``` */ @Directive({ selector: '[ndDropdownMenuRadioGroup]', hostDirectives: [ { directive: NdMenuRadioGroup, inputs: ['value'], outputs: ['valueChange'], }, ], host: { 'data-slot': 'dropdown-menu-radio-group' }, }) export class DropdownMenuRadioGroup {} /** * One choice of a radio group (`role="menuitemradio"`). Put an indicator * with a small filled circle in it. * * @example * ```html * <div ndDropdownMenuRadioItem value="top">Top</div> * ``` */ @Directive({ selector: '[ndDropdownMenuRadioItem]', hostDirectives: [ { directive: NdMenuItemRadio, inputs: ['value', 'disabled'], outputs: ['triggered'], }, ], host: { 'data-slot': 'dropdown-menu-radio-item', '[class]': 'classes()' }, }) export class DropdownMenuRadioItem { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", this.class(), ), ); } /** * The check or dot at the start of a checkbox or radio item, shown while it * is checked. * * @example * ```html * <span ndDropdownMenuItemIndicator><svg ndIcon="check"></svg></span> * ``` */ @Directive({ selector: '[ndDropdownMenuItemIndicator]', hostDirectives: [NdMenuItemIndicator], host: { 'data-slot': 'dropdown-menu-item-indicator', '[class]': 'classes()' }, }) export class DropdownMenuItemIndicator { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'pointer-events-none absolute start-2 flex size-3.5 items-center justify-center', this.class(), ), ); } /** * A heading for a group of items; point the group's `aria-labelledby` at * it. * * @example * ```html * <div ndDropdownMenuLabel id="account">My account</div> * ``` */ @Directive({ selector: '[ndDropdownMenuLabel]', hostDirectives: [NdMenuLabel], host: { 'data-slot': 'dropdown-menu-label', '[attr.data-inset]': 'inset() ? "" : null', '[class]': 'classes()', }, }) export class DropdownMenuLabel { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('px-2 py-1.5 text-sm font-medium data-[inset]:ps-8', this.class()), ); } /** * A line between groups of items (`role="separator"`). * * @example * ```html * <div ndDropdownMenuSeparator></div> * ``` */ @Directive({ selector: '[ndDropdownMenuSeparator]', hostDirectives: [NdMenuSeparator], host: { 'data-slot': 'dropdown-menu-separator', '[class]': 'classes()' }, }) export class DropdownMenuSeparator { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('-mx-1 my-1 h-px bg-border', this.class()), ); } /** * The keyboard shortcut of an item, at its end, as a reminder; the menu * doesn't handle it. * * @example * ```html * <span ndDropdownMenuShortcut>⌘S</span> * ``` */ @Directive({ selector: '[ndDropdownMenuShortcut]', host: { 'data-slot': 'dropdown-menu-shortcut', '[class]': 'classes()' }, }) export class DropdownMenuShortcut { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('ms-auto text-xs tracking-widest text-muted-foreground', this.class()), ); } /** * An item that opens the submenu in its template. Put a chevron icon at its * end with `ms-auto`, turned around in right-to-left layouts. * * @example * ```html * <div [ndDropdownMenuSubTrigger]="more">More tools</div> * ``` */ @Directive({ selector: '[ndDropdownMenuSubTrigger]', hostDirectives: [ { directive: NdMenuSubTrigger, inputs: ['ndMenuSubTrigger: ndDropdownMenuSubTrigger', 'disabled'], }, ], host: { 'data-slot': 'dropdown-menu-sub-trigger', '[attr.data-inset]': 'inset() ? "" : null', '[class]': 'classes()', }, }) export class DropdownMenuSubTrigger { /** Indents it to line up with items that have an indicator. */ readonly inset = input(false, { transform: booleanAttribute }); /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:ps-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { 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
Dropdown Menu checkboxes
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);
}
Dropdown Menu radio group
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 |
|---|---|
| EnterorSpace | Opens the menu and focuses its first item. |
| ArrowDown | Opens the menu and focuses its first item. |
| ArrowUp | Opens the menu and focuses its last item. |
| ArrowDownorArrowUp | Moves focus to the next or previous item, wrapping. |
| HomeorEnd | Moves focus to the first or last item. |
| Enter | Activates the focused item and closes the menu. |
| Space | Activates the focused item; checkbox and radio items keep the menu open. |
| ArrowRight | On a sub-trigger, opens its menu and focuses the first item. |
| ArrowLeft | In a submenu, closes it and focuses its sub-trigger. |
| Escape | Closes the current menu and returns focus to what opened it. |
| Tab | Closes every open menu and moves focus on from the trigger. |
| a | Typeahead: moves focus to the next item whose label starts with the typed letters. |
Focus
- Closing the menu returns focus to the trigger.
- Opened from the keyboard, a menu focuses its first item (its last with
ArrowUpon a Dropdown Menu trigger); opened with the pointer, the menu itself. - Closing returns focus to the trigger or sub-trigger.
Right-to-left
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> | null | null |
[defaultOpen]Initial open state when open isn't bound, applied after hydration. | boolean | false |
[side]Side of the trigger the menu opens on, before flipping to fit. | 'bottom' | 'top' | 'start' | 'end' | 'bottom' |
[align]Alignment of the menu along that side. | 'start' | 'end' | 'center' | 'start' |
[sideOffset]Gap between the trigger and the menu, in pixels. | number | 4 |
[(open)]Whether the menu is open. Two-way bindable with [(open)]. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-trigger |
data-state | — |
DropdownMenuContent
[ndDropdownMenuContent]role menuA 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-slot | dropdown-menu-content |
data-state | — |
data-side | — |
DropdownMenuSubContent
[ndDropdownMenuSubContent]role menuA 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-slot | dropdown-menu-sub-content |
data-state | — |
data-side | — |
DropdownMenuGroup
[ndDropdownMenuGroup]role groupGroups related items (role="group"). Name it with aria-labelledby
pointing at a label.
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-group |
DropdownMenuItem
[ndDropdownMenuItem]role menuitemOne action (role="menuitem"): activating it emits triggered and
closes the menu. Disabled items stay focusable and do nothing. Use a
div: on a button, Space would close the menu.
Manages aria-disabled.
| DropdownMenuItem binding | Type | Default |
|---|---|---|
[inset]Indents it to line up with items that have an indicator. | boolean | false |
[variant]destructive colors a dangerous action, such as Delete. | 'default' | 'destructive' | 'default' |
[class]Extra classes, merged last so they win. | string | '' |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-item |
data-inset | — |
data-variant | — |
data-disabled | — |
DropdownMenuCheckboxItem
[ndDropdownMenuCheckboxItem]role menuitemcheckboxAn item that is on or off (role="menuitemcheckbox"), bound with
[(checked)]. Put an indicator with a check icon in it.
Manages aria-checked, aria-disabled.
| DropdownMenuCheckboxItem binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
[(checked)]Whether it's on. Two-way bindable with [(checked)]. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-checkbox-item |
data-state | — |
data-disabled | — |
DropdownMenuRadioGroup
[ndDropdownMenuRadioGroup]role groupGroups 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 | null | null |
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-radio-group |
DropdownMenuRadioItem
[ndDropdownMenuRadioItem]role menuitemradioOne 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]requiredThe value its group takes when it's checked. | string | — |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | dropdown-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-slot | dropdown-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. | boolean | false |
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | dropdown-menu-label |
data-inset | — |
DropdownMenuSeparator
[ndDropdownMenuSeparator]role separatorA line between groups of items (role="separator").
| DropdownMenuSeparator binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | dropdown-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-slot | dropdown-menu-shortcut |
DropdownMenuSubTrigger
[ndDropdownMenuSubTrigger]role menuitemAn item that opens the submenu in its template. Put a chevron icon at its
end with ms-auto, turned around in right-to-left layouts.
Manages aria-haspopup, aria-expanded, aria-controls.
| DropdownMenuSubTrigger binding | Type | Default |
|---|---|---|
[inset]Indents it to line up with items that have an indicator. | boolean | false |
[class]Extra classes, merged last so they win. | string | '' |
[ndDropdownMenuSubTrigger]The ng-template of the submenu. | TemplateRef<unknown> | null | null |
[disabled]Makes it inert but focusable, with aria-disabled. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | dropdown-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.