Menu
The Menu component presents a structured list of actions, settings, or navigation targets within a transient surface. It supports icons, keyboard shortcuts, hint text, section titles, dividers, and submenu indicators.
import {Menu} from "@qualcomm-ui/react/menu"Examples
Item Customization
Compose the menu to include icons and commands.
import {Component} from "@angular/core"
import {Command, File, FileText, FolderOpen, ImageDown} from "lucide-angular"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, ButtonModule, PortalComponent, IconDirective],
providers: [provideIcons({Command, File, FileText, FolderOpen, ImageDown})],
selector: "menu-item-customization-demo",
template: `
<q-menu>
<button emphasis="primary" q-menu-button variant="fill">Show Menu</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">
<div icon="FileText" q-menu-item-start-icon></div>
New Text File
<div q-menu-item-command>
<svg qIcon="Command" size="xs"></svg>
E
</div>
</button>
<button q-menu-item value="new-file">
<div icon="File" q-menu-item-start-icon></div>
New File...
<div q-menu-item-command>
<svg qIcon="Command" size="xs"></svg>
N
</div>
</button>
<button q-menu-item value="open-file">
<div icon="FolderOpen" q-menu-item-start-icon></div>
Open File...
<div q-menu-item-command>
<svg qIcon="Command" size="xs"></svg>
O
</div>
</button>
<button q-menu-item value="export">
<div icon="ImageDown" q-menu-item-start-icon></div>
Export
<div q-menu-item-command>
<svg qIcon="Command" size="xs"></svg>
S
</div>
</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuItemCustomizationDemo {}Context Menu
Demonstrates how to trigger a menu on right-click or other context events.
import {Component} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, PortalComponent],
selector: "menu-context-menu-demo",
template: `
<q-menu class="w-full">
<button
class="flex h-48 w-full flex-1 items-center justify-center border border-dashed"
q-menu-context-trigger
>
Right click here
</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuContextMenuDemo {}Nested Menus
Compose nested menus by nesting <Menu.Root> components inside each other.
<q-menu>
<button emphasis="primary" q-menu-button variant="fill">Show Menu</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<q-menu>
<button q-menu-trigger-item value="open-recent">
Open Recents
</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="file-1">File 1</button>
<button q-menu-item value="file-2">File 2</button>
<button q-menu-item value="file-3">File 3</button>
</div>
</div>
</q-portal>
</q-menu>
</div>
</div>
</q-portal>
</q-menu>
Links
Menu items can be links.
<div q-menu-content>
<a
href="https://angular.dev"
q-menu-item
target="_blank"
value="angular-dev"
>
<div icon="ExternalLink" q-menu-item-start-icon></div>
angular.dev
</a>
<a
fragment="links"
q-menu-item
routerLink="/components/menu"
value="menu-links-demo"
>
Menu Links Demo
</a>
</div>
Groups
Use the q-menu-item-group component to group menu items.
Item Types
There are three item types, each corresponding to one of the three types of aria menu item roles:
q-menu-item—menuitemq-menu-radio-item—menuitemradioq-menu-checkbox-item—menuitemcheckbox
Checkbox Items
Use checkbox items when you need options that are individually selectable.
import {Component} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, PortalComponent],
selector: "menu-checkbox-items-demo",
template: `
<q-menu>
<button emphasis="primary" q-menu-button variant="fill">Show Menu</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<div q-menu-item-group>
<label q-menu-item-group-label>Choose an option</label>
<button q-menu-checkbox-item value="option-1">
Option 1
<div q-menu-item-indicator></div>
</button>
<button q-menu-checkbox-item value="option-2">
Option 2
<div q-menu-item-indicator></div>
</button>
<button q-menu-checkbox-item value="option-3">
Option 3
<div q-menu-item-indicator></div>
</button>
</div>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuCheckboxItemsDemo {}Checkbox Group State
Checkbox item state is controlled individually on each item. Use the useCheckboxGroup hook to manage the state of a group of checkbox items.
<div q-menu-item-group>
<label q-menu-item-group-label>Choose an option</label>
@for (item of items; track item.value) {
<button
q-menu-checkbox-item
[checked]="checkboxGroup.isChecked(item.value)"
[value]="item.value"
(checkedChanged)="checkboxGroup.toggleValue(item.value)"
>
{{ item.label }}
<div q-menu-item-indicator></div>
</button>
}
</div>
Radio Group
Radio items are grouped such that only one can be selected at a time.
<div q-menu-radio-item-group>
<label q-menu-item-group-label>Choose an option</label>
<div q-menu-radio-item-group>
<button q-menu-radio-item value="option-1">
Option 1
<div q-menu-item-indicator></div>
</button>
<button q-menu-radio-item value="option-2">
Option 2
<div q-menu-item-indicator></div>
</button>
<button q-menu-radio-item value="option-3">
Option 3
<div q-menu-item-indicator></div>
</button>
</div>
</div>
Radio Group State
Radio item state can be controlled via the value property on the parent menu-radio-item-group element.
<div q-menu-radio-item-group>
<label q-menu-item-group-label>Choose an option</label>
<div q-menu-radio-item-group [(value)]="value">
<button q-menu-radio-item value="option-1">
Option 1
<div q-menu-item-indicator></div>
</button>
<button q-menu-radio-item value="option-2">
Option 2
<div q-menu-item-indicator></div>
</button>
<button q-menu-radio-item value="option-3">
Option 3
<div q-menu-item-indicator></div>
</button>
</div>
</div>
Controlled State
The menu's visibility can be controlled via the open, openChange, and defaultOpen properties.
- Use
opento explicitly set whether the menu is open (controlled mode). - Use
openChangedto be notified when the menu requests to open or close. - Use
defaultOpento set the initial open state when you don't need to control it yourself (uncontrolled mode).
In controlled mode, the open prop should be updated in response to openChanged events to reflect the desired visibility.
import {Component, signal} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, PortalComponent],
selector: "menu-controlled-state-demo",
template: `
<q-menu [open]="open()" (openChanged)="open.set($event)">
<button emphasis="primary" q-menu-button variant="fill">Show Menu</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuControlledStateDemo {
protected readonly open = signal(false)
}Placement
Configure the menu's placement using the positioning.placement prop.
import {Component} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, PortalComponent],
selector: "menu-placement-demo",
template: `
<q-menu [positioning]="{placement: 'right-start'}">
<button emphasis="primary" q-menu-button variant="fill">Show Menu</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuPlacementDemo {}Anchor Point
Use the positioning.anchorPoint prop to control the anchor point of the menu.
import {Component, computed, type ElementRef, viewChild} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
import type {PositioningOptions} from "@qualcomm-ui/dom/floating-ui"
@Component({
imports: [MenuModule, PortalComponent],
selector: "menu-anchor-point-demo",
template: `
<div class="flex flex-col gap-4">
<q-menu [positioning]="positioning()">
<button emphasis="primary" q-menu-button variant="fill">
Show Menu
</button>
<div
#anchorRef
class="font-body-lg text-neutral-primary bg-neutral-04 border-neutral-01 rounded-md border p-4"
>
Anchor
</div>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-portal>
</q-menu>
</div>
`,
})
export class MenuAnchorPointDemo {
protected readonly anchorRef =
viewChild.required<ElementRef<HTMLDivElement>>("anchorRef")
protected readonly positioning = computed<PositioningOptions>(() => ({
getAnchorRect: () => this.anchorRef().nativeElement.getBoundingClientRect(),
}))
}Avatar
Here's an example that composes the Menu with the Avatar component to display a user account menu.
import {Component} from "@angular/core"
import {LogOut, Settings, User} from "lucide-angular"
import {AvatarModule} from "@qualcomm-ui/angular/avatar"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [AvatarModule, MenuModule, PortalComponent],
providers: [provideIcons({LogOut, Settings, User})],
selector: "menu-avatar-demo",
template: `
<q-menu [positioning]="{placement: 'right-start'}">
<button q-avatar q-menu-trigger status="active">
<img alt="John Doe" q-avatar-image src="/images/avatar-man.png" />
<div q-avatar-content>JD</div>
<div q-avatar-status></div>
</button>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="account">
<div icon="User" q-menu-item-start-icon></div>
Account
</button>
<button q-menu-item value="settings">
<div icon="Settings" q-menu-item-start-icon></div>
Settings
</button>
<button q-menu-item value="logout">
<div icon="LogOut" q-menu-item-start-icon></div>
Logout
</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuAvatarDemo {}Within Dialog
When using the Menu inside a Dialog, don't render the q-menu-positioner within a <q-portal> element.
import {Component} from "@angular/core"
import {X} from "lucide-angular"
import {ButtonModule} from "@qualcomm-ui/angular/button"
import {DialogModule} from "@qualcomm-ui/angular/dialog"
import {IconDirective} from "@qualcomm-ui/angular/icon"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {provideIcons} from "@qualcomm-ui/angular-core/lucide"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [
MenuModule,
ButtonModule,
DialogModule,
IconDirective,
PortalComponent,
],
providers: [provideIcons({X})],
selector: "menu-within-dialog-demo",
template: `
<div q-dialog-root>
<button emphasis="primary" q-button q-dialog-trigger variant="fill">
Show Dialog
</button>
<q-portal>
<div q-dialog-backdrop></div>
<div q-dialog-positioner>
<section class="w-72" q-dialog-content>
<h3 q-dialog-heading>Dialog Title</h3>
<q-menu>
<button
class="place-self-start"
emphasis="primary"
q-menu-button
variant="fill"
>
Show Menu
</button>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">
New Text File
</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-menu>
<button q-dialog-close-trigger>
<svg qIcon="X"></svg>
</button>
</section>
</div>
</q-portal>
</div>
`,
})
export class MenuWithinDialogDemo {}Hide When Detached
Use the positioning.hideWhenDetached prop to hide the menu when it's detached from the trigger.
import {Component, signal} from "@angular/core"
import {MenuModule} from "@qualcomm-ui/angular/menu"
import {IntersectionObserverDirective} from "@qualcomm-ui/angular-core/observers"
import {PortalComponent} from "@qualcomm-ui/angular-core/portal"
@Component({
imports: [MenuModule, PortalComponent, IntersectionObserverDirective],
selector: "menu-hide-when-detached-demo",
template: `
<q-menu [open]="open()" [positioning]="{hideWhenDetached: true}">
<div
class="border-neutral-03 flex max-w-72 gap-2 overflow-x-scroll rounded-md border p-4"
>
@for (item of items; track item) {
<div
class="font-body-md text-neutral-primary border-neutral-01 bg-neutral-04 rounded-md border p-3 whitespace-nowrap"
>
Item {{ item }}
</div>
}
<button
class="whitespace-nowrap"
emphasis="primary"
q-menu-button
variant="fill"
(qIntersectionObserver)="handleIntersection($event)"
>
Show Menu
</button>
</div>
<q-portal>
<div q-menu-positioner>
<div q-menu-content>
<button q-menu-item value="new-text-file">New Text File</button>
<button q-menu-item value="new-file">New File...</button>
<button q-menu-item value="open-file">Open File...</button>
<button q-menu-item value="export">Export</button>
</div>
</div>
</q-portal>
</q-menu>
`,
})
export class MenuHideWhenDetachedDemo {
items = [...Array(6).keys()]
readonly open = signal(false)
handleIntersection(entries: IntersectionObserverEntry[]) {
if (entries[0]?.isIntersecting) {
// only open the menu when the button is in view,
// but keep it open after to demonstrate the use case.
this.open.set(true)
}
}
}API
<q-menu>
| Prop | Type | Default |
|---|---|---|
The positioning point for the menu. Can be set by the context menu trigger or
the button trigger. | { | |
Whether to close the menu when an option is selected | boolean | true |
Whether the menu is composed with other composite widgets like a combobox or tabs | boolean | true |
The initial highlighted value of the menu item when rendered.
Use when you don't need to control the highlighted value of the menu item. | string | |
The initial open state of the menu when rendered.
Use when you don't need to control the open state of the menu. | boolean | |
The document's text/writing direction. | 'ltr' | 'rtl' | "ltr" |
A root node to correctly resolve the Document in custom environments. i.e.,
Iframes, Electron. | () => | |
The controlled highlighted value of the menu item. | string | |
id attribute. If
omitted, a unique identifier will be generated for accessibility.) | string | |
Whether to synchronize the present change immediately or defer it to the next
frame. | boolean | false |
When true, the component will not be rendered in the DOM until it becomes
visible or active. | boolean | false |
Whether to loop the keyboard navigation. | boolean | false |
The controlled open state of the menu | boolean | |
The options used to dynamically position the menu | ||
The controlled presence of the node. | boolean | |
'sm' | 'md' | ||
Whether to allow the initial presence animation. | boolean | false |
Whether the pressing printable characters should trigger typeahead navigation | boolean | true |
When true, the component will be completely removed from the DOM when it
becomes inactive or hidden, rather than just being hidden with CSS. | boolean | false |
Function called when the escape key is pressed | KeyboardEvent | |
Function called when the animation ends in the closed state | void | |
Function called when the focus is moved outside the component | CustomEvent<{ | |
Function called when the highlighted menu item changes. | string | |
Function called when an interaction happens outside the component | | CustomEvent<{event?: E}> | |
Function to navigate to the selected item if it's an anchor element | { | |
Function called when the open state changes | boolean | |
Function called when the pointer is pressed down outside the component | CustomEvent<{ | |
Function called when this layer is closed due to a parent layer being closed | CustomEvent<{ | |
Function called when a menu item is selected. | string |
{
x: number
y: number
}
booleanbooleanstringboolean'ltr' | 'rtl'
() =>
| Node
| ShadowRoot
| Document
stringstringbooleanbooleanbooleanbooleanboolean'sm' | 'md'
booleanbooleanbooleanKeyboardEventvoidCustomEvent<{
event?: E
}>
string| CustomEvent<{event?: E}>
| CustomEvent<{event?: E}>
{
href: string
node: HTMLAnchorElement
value: string
}
booleanCustomEvent<{
event?: E
}>
CustomEvent<{
originalIndex: number
originalLayer: HTMLElement
targetIndex: number
targetLayer: HTMLElement
}>
stringq-menu-item
| Prop | Type |
|---|---|
The unique value of the menu item option. | string |
Whether the menu should be closed when the option is selected. | boolean |
Whether the menu item is disabled | boolean |
The textual value of the option. Used in typeahead navigation of the menu.
If not provided, the text content of the menu item will be used. | string |
Emitted when the item is selected | void |
stringbooleanbooleanstringvoidq-menu-checkbox-item
| Prop | Type |
|---|---|
The unique value of the menu item option. | string |
Whether the option is checked | boolean |
Whether the menu should be closed when the option is selected. | boolean |
boolean | |
Whether the menu item is disabled | boolean |
The textual value of the option. Used in typeahead navigation of the menu.
If not provided, the text content of the menu item will be used. | string |
boolean | |
Emitted when the item is selected | void |
stringbooleanbooleanbooleanbooleanstringbooleanvoidPositioningOptions
The positioning prop accepts an object with the following shape:
| Prop | Type | Default |
|---|---|---|
The minimum padding between the arrow and the floating element's corner. | number | 4 |
The overflow boundary of the reference element | () => | |
Whether the popover should fit the viewport. | boolean | |
Whether to flip the placement when the floating element overflows the boundary. | | boolean | true |
Function that returns the anchor rect | ( | |
The main axis offset or gap between the reference and floating element | number | 8 |
Whether the popover should be hidden when the reference element is detached | boolean | |
Options to activate auto-update listeners | | boolean | true |
The offset of the floating element | { | |
Function called when the placement is computed | ( | |
Function called when the floating element is positioned or not | (data: { | |
The virtual padding around the viewport edges to check for overflow | number | |
Whether the floating element can overlap the reference element | boolean | false |
The initial placement of the floating element | | 'bottom' | 'bottom' |
Whether to make the floating element same width as the reference element | boolean | |
The secondary axis offset or gap between the reference and floating elements | number | |
Whether the popover should slide when it overflows. | boolean | |
The strategy to use for positioning | | 'absolute' | 'absolute' |
A callback that will be called when the popover needs to calculate its
position. | (data: { |
number() =>
| 'clippingAncestors'
| Element
| Array<Element>
| {
height: number
width: number
x: number
y: number
}
boolean| boolean
| Array<
| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
>
(
element:
| HTMLElement
| VirtualElement,
) => {
height?: number
width?: number
x?: number
y?: number
}
numberboolean| boolean
| {
ancestorResize?: boolean
ancestorScroll?: boolean
animationFrame?: boolean
elementResize?: boolean
layoutShift?: boolean
}
{
crossAxis?: number
mainAxis?: number
}
(
data: ComputePositionReturn,
) => void
(data: {
placed: boolean
}) => void
numberboolean| 'bottom'
| 'bottom-end'
| 'bottom-start'
| 'left'
| 'left-end'
| 'left-start'
| 'right'
| 'right-end'
| 'right-start'
| 'top'
| 'top-end'
| 'top-start'
booleannumberboolean| 'absolute'
| 'fixed'
(data: {
updatePosition: () => Promise<void>
}) => void | Promise<void>