Primitives
A menu is a list of options or commands presented to the user in a dropdown list.
Import the Menu primitives from ng-primitives/menu.
import {
NgpMenu,
NgpMenuItem,
NgpMenuTrigger,
NgpMenuTriggerGroup,
NgpSubmenuTrigger,
NgpMenuItemCheckbox,
NgpMenuItemRadioGroup,
NgpMenuItemRadio,
NgpMenuItemIndicator,
} from 'ng-primitives/menu';
Assemble the menu directives in your template.
<button [ngpMenuTrigger]="menu" ngpButton></button>
<ng-template #menu>
<div ngpMenu>
<button ngpMenuItem>Item 1</button>
<button ngpMenuItem>Item 2</button>
<button ngpMenuItem>Item 3</button>
</div>
</ng-template>
Create reusable components that use the NgpMenu directive.
Generate a reusable menu component using the Angular CLI.
ng g ng-primitives:primitive menu
path: The path at which to create the component file.prefix: The prefix to apply to the generated component selector.component-suffix: The suffix to apply to the generated component class name.file-suffix: The suffix to apply to the generated component file name. Defaults to component.styles: How component styles should be generated. css (default) includes the full example styles; unstyled omits them entirely so you can style the component yourself.example-styles (deprecated): still supported for compatibility - true maps to styles: css, false maps to styles: unstyled.Here are some additional examples of how to use the Menu primitives.
The menu can contain submenus, which are nested menus that can be opened by hovering on a menu item.
Menu items that can be toggled on and off. Clicking a checkbox item does not close the menu.
Menu items that allow selecting one option from a group. Clicking a radio item does not close the menu.
You can customize the offset using either a simple number or an object for more precise control:
<!-- Simple number offset -->
<button [ngpMenuTrigger]="menu" ngpMenuTriggerOffset="12">Menu with 12px offset</button>
<!-- Object offset for precise control -->
<button
[ngpMenuTrigger]="menu"
[ngpMenuTriggerOffset]="{mainAxis: 8, crossAxis: 4, alignmentAxis: 2}"
>
Menu with custom offset
</button>
You can customize the shift behavior to control how the menu stays within the viewport:
<!-- Disable shift -->
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerShift]="false">Menu without shift</button>
<!-- Object shift for precise control -->
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerShift]="{padding: 8}">
Menu with custom shift padding
</button>
By default, the menu is kept within the viewport and its clipping ancestors. Pass boundary or rootBoundary to flip or shift to measure overflow against something else:
<!-- Keep the menu inside a container element -->
<div #boundary>
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerFlip]="{boundary: boundary}">
Menu constrained to a container
</button>
</div>
<!-- Keep the menu inside whatever scrolls the trigger -->
<div style="overflow: auto">
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerFlip]="{altBoundary: true}">
Menu constrained to the trigger's scroll container
</button>
</div>
<!-- Measure against the whole document rather than the viewport -->
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerFlip]="{rootBoundary: 'document'}">
Menu that does not flip while off-screen
</button>
altBoundary measures against the trigger's clipping ancestors rather than the menu's. The menu is portalled to the body, so its own clipping ancestors are effectively the viewport - set this when the trigger sits in a scroll container. It applies only while boundary is left at its default; an explicit boundary always wins.
crossAxis widens the axis each option checks. For flip it is the alignment axis, on by default - that is how bottom-end becomes bottom-start near an edge. For shift it is the side axis, off by default; enabling it lets the panel move along the placement direction too:
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerFlip]="{crossAxis: false}">
Keep the alignment
</button>
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerShift]="{crossAxis: true}">
Shift on both axes
</button>
The --ngp-menu-available-width and --ngp-menu-available-height custom properties are measured against the flip boundary, or the shift boundary when flip does not set one.
You can provide a separate anchor element using ngpMenuTriggerAnchor to control where the menu appears while keeping the full trigger area interactive. Clicks on the anchor are treated as inside the menu, so they do not dismiss it.
The anchor also becomes the element --ngp-menu-trigger-width measures, so a menu sized to that property matches the anchor - which is rarely what you want when the anchor is a small icon.
The anchor is live: rebinding it moves an already-open menu to the new element, and the new element is the one that counts as inside for dismissal. A single menu can therefore be pointed at whichever of many elements was interacted with, instead of giving each one its own trigger.
When one menu serves many targets, re-anchor it with setAnchor on pointerdown rather than by rebinding ngpMenuTriggerAnchor:
<button #trigger="ngpMenuTrigger" [ngpMenuTrigger]="menu">Actions</button>
<button
#token
type="button"
(pointerdown)="trigger.setAnchor(token)"
(click)="trigger.setAnchor(token)"
>
{{ label }}
</button>
Two things have to line up, and the input binding only gives you the first:
mouseup, which runs before a bubbling click, so an anchor claimed on click is claimed too late and the press dismisses the menu you meant to move. pointerdown is early enough, and covers mouse, pen and touch alike - mousedown works too.ngpMenuTriggerAnchor is an input, so a new value only reaches the trigger on the next change detection pass. A press held for a few milliseconds gets one; a fast tap, or a frame the browser is busy rendering hundreds of targets, does not - and the dismissal check then still sees the old anchor. setAnchor writes the value the check reads, so there is no pass to wait for.Keep the click handler as well, so the targets work without a pointer: keyboard activation raises click with no preceding pointerdown. Note that it claims the anchor rather than moving an open menu - activating anything outside an open menu closes it - so from the keyboard the interaction is two steps, claim then open. Targets have to be focusable for any of this to mean anything, so use a native button rather than a span.
Rebinding ngpMenuTriggerAnchor remains the right choice when the anchor changes outside a press - from a selection, a route, or a resize - where nothing is racing the binding.
Guard the re-anchor if pressing the current anchor again should close the menu, since setting it to the element it already points at leaves the menu open.
An anchor that is removed from the DOM closes the menu, so a target recycled out of a virtualized list does not leave the menu stranded at a stale position.
Enable keyboard triggers to allow users to open menus using Enter or arrow keys:
<!-- Enable Enter key to toggle menu -->
<button [ngpMenuTrigger]="menu" [ngpMenuTriggerOpenTriggers]="['click', 'enter']">Menu</button>
<!-- Enable arrow keys (placement-aware) -->
<button
[ngpMenuTrigger]="menu"
[ngpMenuTriggerOpenTriggers]="['arrowkey']"
ngpMenuTriggerPlacement="right-start"
>
Sidebar Menu
</button>
<!-- Combine triggers for best UX -->
<button
[ngpMenuTrigger]="menu"
[ngpMenuTriggerOpenTriggers]="['hover', 'arrowkey', 'enter']"
ngpMenuTriggerPlacement="right-start"
>
Navigation Item
</button>
Enter Key:
Arrow Keys:
bottom-* placement: ArrowDown openstop-* placement: ArrowUp opensright-* placement: ArrowRight opens (ArrowLeft in RTL)left-* placement: ArrowLeft opens (ArrowRight in RTL)In a vertical navigation menu, each top-level item is often its own NgpMenuTrigger that opens a menu to the side on hover. Moving the pointer from the trigger toward that menu can pass over a sibling trigger on the way, since diagonal mouse movement rarely follows a straight line - without protection, that sibling would open its own menu and steal focus from the one you were heading toward.
Nested NgpSubmenuTriggers inside an already-open menu are protected from this automatically. Root-level triggers are not, since they may have no shared parent at all - wrap them in NgpMenuTriggerGroup to opt in:
Moving between siblings closes one menu and opens another in quick succession, so the example also sets ngpMenuTriggerCooldown. Within that window, the swap is treated as a single movement: the outgoing menu is dropped rather than animating out behind its replacement, and the incoming one is marked data-instant so its own animation can be skipped.
The coordination only earns its keep while the siblings actually sit between a trigger and its menu. In a collapsible navigation, that stops being true once the rail collapses to icons - the pointer reaches the panel without crossing anything, so all the group does is hold the siblings inert a moment longer than they need to be. Bind ngpMenuTriggerGroupSiblingTracking to turn it off for as long as that is the case:
<nav ngpMenuTriggerGroup [ngpMenuTriggerGroupSiblingTracking]="!collapsed()">
<!-- triggers -->
</nav>
The following directives are available to import from the ng-primitives/menu package:
For the menu to be positioned correctly relative to the trigger element, it should use fixed positioning. For example, you can use the following CSS:
[ngpMenu] {
position: fixed;
}
The ngpMenu primitive adds a CSS custom property --ngp-menu-transform-origin to the element that can be used to animate the menu from the trigger element.
The ngpMenu will also add the data-enter and data-exit attributes to the element when it is being added or removed from the DOM. This can be used to trigger animations.
:host[data-enter] {
animation: fade-in 0.2s ease-in-out;
}
:host[data-exit] {
animation: fade-out 0.2s ease-in-out;
}
When using the cooldown option to allow quick switching between menus, the data-instant attribute is applied. Use this to skip animations for instant transitions:
:host[data-instant][data-enter] {
animation: none;
}
data-instant comes off as the menu switches to its exit state, so the rule only ever needs the enter state - the exit animation is never suppressed by it.
You can configure the default options for all menus in your application by using the provideMenuConfig function in a providers array.
import { provideMenuConfig } from 'ng-primitives/menu';
bootstrapApplication(AppComponent, {
providers: [
provideMenuConfig({
offset: 4,
placement: 'top',
flip: true,
container: document.body,
scrollBehavior: 'reposition',
cooldown: 0,
}),
],
});
Adheres to the WAI-ARIA Menu Button Pattern.
| Key | Description |
|---|---|
| Enter | Activates the focused menu item. Closes all menus unless the item is a checkbox or radio item. |
| Escape | Closes all open menus and returns focus to the root menu trigger. |
| ArrowDown | Moves focus to the next menu item. |
| ArrowUp | Moves focus to the previous menu item. |
| ArrowRight | Opens a submenu when focused on a submenu trigger. |
| ArrowLeft | Closes the current submenu and moves focus to the parent submenu trigger. |
| Home | Moves focus to the first menu item. |
| End | Moves focus to the last menu item. |
:focus-visible).Copyright © 2026 Angular Primitives
This site is powered by Netlify