On this page
Menu
A click-triggered popup menu driven by the MenuItem model. Supports icons, badges, separators, disabled items, submenus, and full keyboard navigation.
uwc-menu is a popup menu driven by a MenuItem[] property. Place any element in
the trigger slot to open it. It supports separators, badges, disabled items, submenus, and
full keyboard navigation (Arrow keys, Enter, Escape).
Import
All components
import '@uwc/components';
Selected component (Lit / Angular / Vue)
import { UwcMenu } from '@uwc/components/menu';
customElements.define('uwc-menu', UwcMenu);
React
import { UwcMenu } from '@uwc/components/react';
Usage
Lit
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('app-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'Save', command: () => console.log('save') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
render() {
return html`
<uwc-menu .items=${this.items}>
<uwc-button slot="trigger" label="Open"></uwc-button>
</uwc-menu>
`;
}
}
React
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'Save', command: () => console.log('save') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
return (
<UwcMenu items={items}>
<uwc-button slot="trigger" label="Open"></uwc-button>
</UwcMenu>
);
}
Angular
import { Component } from '@angular/core';
import '@uwc/menu';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items">
<uwc-button slot="trigger" label="Open"></uwc-button>
</uwc-menu>
`,
})
export class AppComponent {
items = [
{ label: 'Save', command: () => console.log('save') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
}
Vue
import '@uwc/menu';
export default {
data() {
return {
items: [
{ label: 'Save', command: () => console.log('save') },
{ separator: true },
{ label: 'Quit', disabled: true },
],
};
},
template: `
<uwc-menu :items="items">
<uwc-button slot="trigger" label="Open"></uwc-button>
</uwc-menu>
`,
};
Basic Usage
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('app-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Export', items: [{ label: 'As PDF' }, { label: 'As CSV' }, { label: 'As JSON' }] },
{ separator: true },
{ label: 'Quit', disabled: true },
];
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start">
<uwc-button slot="trigger" label="File menu"></uwc-button>
</uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Export', items: [{ label: 'As PDF' }, { label: 'As CSV' }, { label: 'As JSON' }] },
{ separator: true },
{ label: 'Quit', disabled: true },
];
return (
<UwcMenu items={items} placement="bottom-start">
<uwc-button slot="trigger" label="File menu"></uwc-button>
</UwcMenu>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start">
<uwc-button slot="trigger" label="File menu"></uwc-button>
</uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Export', items: [{ label: 'As PDF' }, { label: 'As CSV' }, { label: 'As JSON' }] },
{ separator: true },
{ label: 'Quit', disabled: true },
];
}
export default {
data() {
return {
items: [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Export', items: [{ label: 'As PDF' }, { label: 'As CSV' }, { label: 'As JSON' }] },
{ separator: true },
{ label: 'Quit', disabled: true },
],
};
},
template: `
<uwc-menu :items="items" placement="bottom-start">
<uwc-button slot="trigger" label="File menu"></uwc-button>
</uwc-menu>
`
};
Basic
Pass an array of MenuItem objects to the items property. Use the
trigger slot for the activating element.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('menu-basic-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start">
<uwc-button slot="trigger" label="File"></uwc-button>
</uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
return (
<UwcMenu items={items} placement="bottom-start">
<uwc-button slot="trigger" label="File"></uwc-button>
</UwcMenu>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start">
<uwc-button slot="trigger" label="File"></uwc-button>
</uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Quit', disabled: true },
];
}
export default {
data() {
return {
items: [
{ label: 'New file', command: () => alert('New file') },
{ label: 'Open', badge: '3' },
{ label: 'Save', command: () => alert('Saved') },
{ separator: true },
{ label: 'Quit', disabled: true },
],
};
},
template: `
<uwc-menu :items="items" placement="bottom-start">
<uwc-button slot="trigger" label="File"></uwc-button>
</uwc-menu>
`
};
Badges
Items support a badge label rendered at the trailing edge.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('menu-badges-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'Inbox', badge: '12' },
{ label: 'Starred', badge: 'New' },
{ label: 'Sent' },
{ separator: true },
{ label: 'Spam', badge: '2' },
{ label: 'Archive' },
];
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start">
<uwc-button slot="trigger" label="Mailbox"></uwc-button>
</uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'Inbox', badge: '12' },
{ label: 'Starred', badge: 'New' },
{ label: 'Sent' },
{ separator: true },
{ label: 'Spam', badge: '2' },
{ label: 'Archive' },
];
return (
<UwcMenu items={items} placement="bottom-start">
<uwc-button slot="trigger" label="Mailbox"></uwc-button>
</UwcMenu>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start">
<uwc-button slot="trigger" label="Mailbox"></uwc-button>
</uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'Inbox', badge: '12' },
{ label: 'Starred', badge: 'New' },
{ label: 'Sent' },
{ separator: true },
{ label: 'Spam', badge: '2' },
{ label: 'Archive' },
];
}
export default {
data() {
return {
items: [
{ label: 'Inbox', badge: '12' },
{ label: 'Starred', badge: 'New' },
{ label: 'Sent' },
{ separator: true },
{ label: 'Spam', badge: '2' },
{ label: 'Archive' },
],
};
},
template: `
<uwc-menu :items="items" placement="bottom-start">
<uwc-button slot="trigger" label="Mailbox"></uwc-button>
</uwc-menu>
`
};
Submenus
Nest an items array inside any item to create a submenu — opened on hover or
ArrowRight.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('menu-submenus-demo')
export class AppDemo extends LitElement {
items = [
{
label: 'Edit',
items: [
{ label: 'Cut' },
{ label: 'Copy' },
{ label: 'Paste' },
],
},
{
label: 'View',
items: [
{ label: 'Zoom in', badge: 'Ctrl++' },
{ label: 'Zoom out', badge: 'Ctrl+-' },
{ separator: true },
{ label: 'Full screen' },
],
},
{ separator: true },
{ label: 'Exit' },
];
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start">
<uwc-button slot="trigger" label="Actions"></uwc-button>
</uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{
label: 'Edit',
items: [{ label: 'Cut' }, { label: 'Copy' }, { label: 'Paste' }],
},
{
label: 'View',
items: [
{ label: 'Zoom in', badge: 'Ctrl++' },
{ label: 'Zoom out', badge: 'Ctrl+-' },
{ separator: true },
{ label: 'Full screen' },
],
},
{ separator: true },
{ label: 'Exit' },
];
return (
<UwcMenu items={items} placement="bottom-start">
<uwc-button slot="trigger" label="Actions"></uwc-button>
</UwcMenu>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start">
<uwc-button slot="trigger" label="Actions"></uwc-button>
</uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'Edit', items: [{ label: 'Cut' }, { label: 'Copy' }, { label: 'Paste' }] },
{
label: 'View',
items: [
{ label: 'Zoom in', badge: 'Ctrl++' },
{ label: 'Zoom out', badge: 'Ctrl+-' },
{ separator: true },
{ label: 'Full screen' },
],
},
{ separator: true },
{ label: 'Exit' },
];
}
export default {
data() {
return {
items: [
{ label: 'Edit', items: [{ label: 'Cut' }, { label: 'Copy' }, { label: 'Paste' }] },
{
label: 'View',
items: [
{ label: 'Zoom in', badge: 'Ctrl++' },
{ label: 'Zoom out', badge: 'Ctrl+-' },
{ separator: true },
{ label: 'Full screen' },
],
},
{ separator: true },
{ label: 'Exit' },
],
};
},
template: `
<uwc-menu :items="items" placement="bottom-start">
<uwc-button slot="trigger" label="Actions"></uwc-button>
</uwc-menu>
`
};
Disabled items
Set disabled: true on any item to render it non-interactively.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('menu-disabled-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'Publish', command: () => alert('Publishing!') },
{ label: 'Schedule', disabled: true },
{ label: 'Save draft' },
{ separator: true },
{ label: 'Delete', disabled: true },
];
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start">
<uwc-button slot="trigger" label="Post actions"></uwc-button>
</uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'Publish', command: () => alert('Publishing!') },
{ label: 'Schedule', disabled: true },
{ label: 'Save draft' },
{ separator: true },
{ label: 'Delete', disabled: true },
];
return (
<UwcMenu items={items} placement="bottom-start">
<uwc-button slot="trigger" label="Post actions"></uwc-button>
</UwcMenu>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start">
<uwc-button slot="trigger" label="Post actions"></uwc-button>
</uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'Publish', command: () => alert('Publishing!') },
{ label: 'Schedule', disabled: true },
{ label: 'Save draft' },
{ separator: true },
{ label: 'Delete', disabled: true },
];
}
export default {
data() {
return {
items: [
{ label: 'Publish', command: () => alert('Publishing!') },
{ label: 'Schedule', disabled: true },
{ label: 'Save draft' },
{ separator: true },
{ label: 'Delete', disabled: true },
],
};
},
template: `
<uwc-menu :items="items" placement="bottom-start">
<uwc-button slot="trigger" label="Post actions"></uwc-button>
</uwc-menu>
`
};
External trigger
Wire the menu to any element on the page via trigger-id.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('menu-external-demo')
export class AppDemo extends LitElement {
items = [
{ label: 'Rename' },
{ label: 'Move to' },
{ separator: true },
{ label: 'Delete' },
];
render() {
return html`
<uwc-button id="ctx-trigger" label="Actions"></uwc-button>
<uwc-menu .items=${this.items} trigger-id="ctx-trigger" placement="bottom-start"></uwc-menu>
`;
}
}
import React from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const items = [
{ label: 'Rename' },
{ label: 'Move to' },
{ separator: true },
{ label: 'Delete' },
];
return (
<>
<uwc-button id="ctx-trigger" label="Actions"></uwc-button>
<UwcMenu items={items} triggerId="ctx-trigger" placement="bottom-start" />
</>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-button id="ctx-trigger" label="Actions"></uwc-button>
<uwc-menu [items]="items" trigger-id="ctx-trigger" placement="bottom-start"></uwc-menu>
`
})
export class AppComponent {
items = [
{ label: 'Rename' },
{ label: 'Move to' },
{ separator: true },
{ label: 'Delete' },
];
}
export default {
data() {
return {
items: [
{ label: 'Rename' },
{ label: 'Move to' },
{ separator: true },
{ label: 'Delete' },
],
};
},
template: `
<uwc-button id="ctx-trigger" label="Actions"></uwc-button>
<uwc-menu :items="items" trigger-id="ctx-trigger" placement="bottom-start"></uwc-menu>
`
};
Listening to events
uwc-item-select fires with { item, originalEvent } on every selection.
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
@customElement('menu-events-demo')
export class AppDemo extends LitElement {
static styles = css`
:host { display: block; }
.log {
margin-top: .75rem; padding: .6rem .75rem; border-radius: 6px;
background: #f9fafb; border: 1px solid #e5e7eb;
font-size: .8rem; font-family: monospace; min-height: 2.5rem; color: #374151;
}
`;
@state() private _log: string[] = [];
items = [
{ label: 'Copy link', id: 'copy' },
{ label: 'Share', id: 'share' },
{ separator: true },
{ label: 'Report', id: 'report' },
];
private _onSelect(e: CustomEvent) {
const time = new Date().toLocaleTimeString();
this._log = [`[${time}] selected: "${e.detail.item.label}"`, ...this._log].slice(0, 5);
}
render() {
return html`
<uwc-menu .items=${this.items} placement="bottom-start" @uwc-item-select=${this._onSelect.bind(this)}>
<uwc-button slot="trigger" label="More..."></uwc-button>
</uwc-menu>
<div class="log">
${this._log.length
? this._log.map(l => html`<div>${l}</div>`)
: html`<span style="color:#9ca3af">Select an item to see events...</span>`}
</div>
`;
}
}
import React, { useState } from 'react';
import { UwcMenu } from '@uwc/components/react';
export default function App() {
const [log, setLog] = useState([]);
const items = [
{ label: 'Copy link', id: 'copy' },
{ label: 'Share', id: 'share' },
{ separator: true },
{ label: 'Report', id: 'report' },
];
return (
<>
<UwcMenu
items={items}
placement="bottom-start"
onUwcItemSelect={(e) => {
const time = new Date().toLocaleTimeString();
setLog(prev => [`[${time}] selected: "${e.detail.item.label}"`, ...prev].slice(0, 5));
}}
>
<uwc-button slot="trigger" label="More..."></uwc-button>
</UwcMenu>
<div style={{ marginTop: '.75rem', fontFamily: 'monospace', fontSize: '.8rem' }}>
{log.length
? log.map((l, i) => <div key={i}>{l}</div>)
: <span style={{ color: '#9ca3af' }}>Select an item to see events...</span>}
</div>
</>
);
}
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-menu [items]="items" placement="bottom-start" (uwc-item-select)="onSelect($event)">
<uwc-button slot="trigger" label="More..."></uwc-button>
</uwc-menu>
<div style="margin-top:.75rem;font-family:monospace;font-size:.8rem">
@for (entry of log(); track entry) {
<div>{{ entry }}</div>
}
@if (!log().length) {
<span style="color:#9ca3af">Select an item to see events...</span>
}
</div>
`
})
export class AppComponent {
readonly log = signal<string[]>([]);
items = [
{ label: 'Copy link' },
{ label: 'Share' },
{ separator: true },
{ label: 'Report' },
];
onSelect(e: CustomEvent) {
const time = new Date().toLocaleTimeString();
this.log.set([`[${time}] selected: "${e.detail.item.label}"`, ...this.log()].slice(0, 5));
}
}
export default {
data() {
return {
log: [],
items: [
{ label: 'Copy link' },
{ label: 'Share' },
{ separator: true },
{ label: 'Report' },
],
};
},
methods: {
onSelect(e) {
const time = new Date().toLocaleTimeString();
this.log = [`[${time}] selected: "${e.detail.item.label}"`, ...this.log].slice(0, 5);
},
},
template: `
<uwc-menu :items="items" placement="bottom-start" @uwc-item-select="onSelect">
<uwc-button slot="trigger" label="More..."></uwc-button>
</uwc-menu>
<div style="margin-top:.75rem;font-family:monospace;font-size:.8rem">
<div v-for="entry in log" :key="entry">{{ entry }}</div>
<span v-if="!log.length" style="color:#9ca3af">Select an item to see events...</span>
</div>
`
};
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
items |
— |
— |
MenuItem[] — the menu item model. |
trigger-id |
— |
— |
External trigger id. |
placement |
— |
— |
Default: bottom-start. |
offset |
— |
— |
Gap px. Default: 6. |
Properties
| Name | Type | Default | Description |
|---|---|---|---|
triggerId |
string | undefined |
— |
— |
items |
MenuItem[] |
[] |
— |
placement |
Placement |
'bottom-start' |
— |
offset |
number |
6 |
— |
isOpen |
boolean |
— |
— |
styles |
array |
[styles] |
— |
Slots
| Name | Description |
|---|---|
trigger |
Element that opens the menu on click. |
Events
| Name | Type | Description |
|---|---|---|
uwc-show |
CustomEvent |
— |
uwc-hide |
CustomEvent |
— |
uwc-item-select |
CustomEvent |
— detail: { item, originalEvent } |
CSS Custom Properties
| Name | Default | Description |
|---|---|---|
--uwc-menu-bg |
— |
— |
--uwc-menu-border |
— |
— |
--uwc-menu-radius |
— |
— |
--uwc-menu-shadow |
— |
— |
--uwc-menu-item-hover-bg |
— |
— |
--uwc-menu-item-hover-color |
— |
— |
--uwc-menu-item-disabled-color |
— |
— |
--uwc-menu-item-font-size |
— |
— |
--uwc-menu-item-danger-color |
— |
— |
--uwc-menu-z |
— |
— |
--uwc-menu-duration |
— |
— |
CSS Parts
| Name | Description |
|---|---|
panel |
— |
item |
— |
item-icon |
— |
item-label |
— |
item-badge |
— |
separator |
— |
submenu-panel |
— |