UWC Components
  • Default
  • Material
  • Fluent

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