Atomic Angular — Doc interne

Menu

An accessible dropdown or contextual menu built on @angular/aria — roving focus, typeahead, unlimited nested submenus, and checkable items with no interactive control inside a menuitem.

A dropdown of actions, a "kebab" row menu, a nested settings menu — Menu composes MenuItems (with optional nested SubMenus) inside MenuContent, wired to a trigger with MenuTriggerDirective. It is itself a popover — opening, positioning, dismissal and focus restoration all come from the same native Popover API Popover is built on, applied as a host directive, so there is no <div popover> wrapper to write yourself.

Concept — roving focus

A menu's items are not each individually tabbable: Tab leaves the whole menu in one step, and ↓/↑/Home/ End move a single "active" focus between items instead. This is the ARIA menu pattern, and @angular/aria/menu implements all of it — the roving focus, the keyboard navigation, and typeahead — so Menu only has to compose the pieces.

Minimal example

[menuTrigger]="menu" wires popovertarget, aria-haspopup="menu" and a reactive aria-expanded onto the button — no manual id to invent or keep in sync. See Pitfalls for the one thing this example would silently break without: the #menu="menu" export form.

How it works

Menu applies two host directives rather than wrapping either: @angular/aria's Menu (roving focus, keyboard navigation, typeahead, the itemSelected output) and this library's own PopoverDirective (positioning, native show/close, the state signal). Selecting a non-submenu item closes the popover; opening a submenu never bubbles into that same close path, so the root stays open while a submenu is showing.

popovertarget submenu select select, bubbles up closes the root "button [menuTrigger Trigger Menu (popover host) MenuContent MenuItem MenuItem with submenu SubMenu MenuItem itemSelected

itemSelected bound only on the root <Menu> still fires for an item selected several SubMenu levels deep — selection bubbles all the way up, so there is never a reason to bind a handler on every nested level.

Recipes

Grouping items, with a separator

MenuContent accepts arbitrary projected content — group items visually with role="group" + a heading span, separated by a .dialog-sep marker div (both pre-styled).

[disabled]="true" excludes an item from keyboard navigation and selection. .heading spans and role="group" wrappers are not focusable and are skipped by the roving navigation.

Nested submenus

Nest a <SubMenu> as a sibling of the <MenuItem> that opens it — never a child — and wire the two together with [submenu]. Submenus can be nested arbitrarily deep by repeating the pattern.

Do not add a <MenuContent> inside <SubMenu> — it is already implicit in the component's own template. Customize a submenu's side with placement (defaults to "right-start", and flips/shifts with Floating UI like the root popover does).

Checkable items — no control inside a menuitem

A menu entry that toggles a state cannot put a control (a checkbox, a switch) inside a MenuItem: a menuitem containing an interactive control is invalid ARIA, and it would leave that control out of the menu's own keyboard navigation. The ARIA APG answer is for the item itself to carry the role and the state — supported through role="menuitemcheckbox"/"menuitemradio" plus a checked input, which also stops the menu from closing when such an item is picked, so several options can be toggled in a row.

The state stays owned by the host: the item emits its value through itemSelected as usual, the host flips its own signal, and checked/the projected Switch reflect it back. presentational on Switch makes its own <input> inert — out of the keyboard path and the accessibility tree, since the MenuItem itself is already the interactive element the switch merely illustrates.

Right-click / context menu

[menuTrigger] only sets popovertarget — the native browser auto-invoke for that attribute only applies to <button>, so a plain element needs to open the menu imperatively, e.g. on contextmenu.

MenuComponent exposes its underlying PopoverDirective through the public popover property (menu.popover.show()/.close()/.toggle()/.state()), which is what makes this kind of imperative control possible.

Give another panel the Menu's look

A panel that is not a Menu — a listbox of search results, a custom popover — can wear the same surface through the class helpers the Menu itself is built from: menuPanelVariants() (rounded, shadowed, padded panel), menuItemVariants() (a row, with hover, active and disabled states), menuContentVariants() and menuSeparatorVariants(). They only return class names; the behaviour — roles, keyboard, positioning — stays yours.

A row that is the current one is tinted like a hovered item: data-active="true" for the Menu's own roving items and for the options of an @angular/aria listbox (which mark the current row that way), aria-selected="true" for a listbox written by hand following the APG.

Options

Prop

Type

itemSelected emits the selected MenuItem's value — never emitted by an item that only opens a submenu. Because the output is exposed through a host directive, Angular does not infer its generic type from the template: type the handler's parameter explicitly to constrain it.

Pitfalls

What's next

On this page