Menu
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
A menu (dropdown menu or context menu) is a popover component that displays a list of actions or options when triggered. Menus provide a compact way to present choices without cluttering the interface.
Examples
Live Preview
Basic Menu
Menu Alignment
The menu-align prop controls horizontal alignment. Use menu-align="right" when the menu button is near the right edge of the viewport.
Menu with Links
Disabled Button
Menu with Disabled Items
Complex Menu (File Menu)
Menu with Sections
Menu with Icon Button
The following examples show using an icon as the trigger. The menu-variant="icon" removes the chevron and provides a minimal button container for the icon. You can also use an icon with the default chevron trigger as we see on the right.
Large
Medium
Small
Monochrome Variant
Event Handling
Menu Types: Navigation vs Selection
The type prop controls selection behavior. Use type="default" (the default) for navigation menus where selection is transient, and type="single-select" for menus where selection should persist (like filters or sorting).
Navigation Menu (type="default")
Selection clears when menu closes. Use for navigation and actions.
Selection Menu (type="single-select")
Selection persists when menu closes. Use for filters, sorting, etc.
Additional Gutter
The additional-gutter prop adds extra vertical spacing beyond the trigger button's height when positioning the menu. This is useful when the menu button is within a taller container (like a header) and you need the menu to clear that container.
This menu has additional-gutter="20px"
Responsive Hidden Items (checkHiddenItems)
The check-hidden-items prop enables the menu to skip items that are hidden via CSS (like responsive media queries). This is useful when you wrap menu items in responsive containers but want keyboard navigation to work correctly.
Performance Note: Enabling this feature checks computed styles on every keyboard navigation, which has a performance cost. Only enable it if you're using CSS-based hiding. For better performance, prefer conditional rendering (v-if) instead.
Try resizing your browser: Desktop items are hidden on mobile (<768px), mobile items are hidden on desktop. Keyboard navigation skips hidden items.
Recommended: Conditional Rendering (Better Performance)
Instead of using check-hidden-items, you can achieve the same result with better performance by using Vue's conditional rendering (v-if). This removes hidden items from the DOM entirely.
Current viewport: Desktop
Dynamic Icon Switching
The menu button exposes a data-menu-open attribute that changes based on the menu state. You can use this with CSS to dynamically switch icons when the menu opens/closes.
Click the icon to see it change from ☰ to ✕
CSS Shadow Parts Customization
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Basic Menu</h2>
</div>
<div class="stacked mbe4">
<VueMenu menu-aria-label="Menu options">
Menu
<template #menu>
<VueMenuItem value="edit">Edit</VueMenuItem>
<VueMenuItem value="copy">Copy</VueMenuItem>
<VueMenuItem value="paste">Paste</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="delete">Delete</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Menu Alignment</h2>
<p class="mbe4">
The <code>menu-align</code> prop controls horizontal alignment. Use <code>menu-align="right"</code> when the menu button is near the right edge of the viewport.
</p>
</div>
<div class="stacked mbe4">
<div
class="flex items-center"
style="gap: 1rem; justify-content: space-between;"
>
<VueMenu
menu-align="left"
menu-aria-label="Left-aligned menu"
>
Left Aligned
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-align="right"
menu-aria-label="Right-aligned menu"
>
Right Aligned
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h2>Menu with Links</h2>
</div>
<div class="stacked mbe4">
<VueMenu menu-aria-label="Navigation menu">
Navigation
<template #menu>
<VueMenuItem
value="home"
href="#home"
>Home</VueMenuItem>
<VueMenuItem
value="about"
href="#about"
>About</VueMenuItem>
<VueMenuItem
value="contact"
href="#contact"
>Contact</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem
value="external"
href="https://example.com"
target="_blank"
>
External Link
</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Disabled Button</h2>
</div>
<div class="stacked mbe4">
<VueMenu
disabled
menu-aria-label="Disabled menu"
>
Disabled Menu
<template #menu>
<VueMenuItem value="item1">Item 1</VueMenuItem>
<VueMenuItem value="item2">Item 2</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Menu with Disabled Items</h2>
</div>
<div class="stacked mbe4">
<VueMenu menu-aria-label="Menu with disabled items">
Mixed States
<template #menu>
<VueMenuItem value="enabled1">Enabled Item</VueMenuItem>
<VueMenuItem
value="disabled1"
:disabled="true"
>Disabled Item</VueMenuItem>
<VueMenuItem value="enabled2">Another Enabled</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem
value="disabled2"
:disabled="true"
>Another Disabled</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Complex Menu (File Menu)</h2>
</div>
<div class="stacked mbe4">
<VueMenu menu-aria-label="File menu">
File
<template #menu>
<VueMenuItem value="new">New</VueMenuItem>
<VueMenuItem value="open">Open...</VueMenuItem>
<VueMenuItem value="recent">Open Recent</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="save">Save</VueMenuItem>
<VueMenuItem value="save-as">Save As...</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="export">Export</VueMenuItem>
<VueMenuItem value="import">Import</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="print">Print</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="close">Close</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Menu with Sections</h2>
</div>
<div class="stacked mbe4">
<VueMenu menu-aria-label="User menu">
<div class="flex-inline items-center">
<User
:size="16"
class="mie2"
/>
User Menu
</div>
<template #menu>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="help">Help</VueMenuItem>
<VueMenuItem value="feedback">Send Feedback</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Menu with Icon Button</h2>
<p class="mbe4">
The following examples show using an icon as the trigger. The <code>menu-variant="icon"</code> removes the chevron and provides a minimal button container for the icon. You can also use an
icon with the default chevron trigger as we see on the right.
</p>
</div>
<div class="mbe4">
<h3>Large</h3>
<div
class="flex-inline items-center"
style="gap: 1rem;"
>
<!-- Icon only -->
<VueMenu
menu-variant="icon"
ghost
menu-aria-label="More options menu"
>
<Menu :size="24" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
<!-- Icon with chevron -->
<VueMenu menu-aria-label="More options menu">
<Menu :size="24" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h3>Medium</h3>
<div
class="flex-inline items-center"
style="gap: 1rem;"
>
<!-- Icon only -->
<VueMenu
menu-variant="icon"
ghost
size="sm"
menu-aria-label="More options menu"
>
<Menu :size="18" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
<!-- Icon with chevron -->
<VueMenu
size="sm"
menu-aria-label="More options menu"
>
<Menu :size="18" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h3>Small</h3>
<div
class="flex-inline items-center"
style="gap: 1rem;"
>
<!-- Icon only -->
<VueMenu
menu-variant="icon"
ghost
size="x-sm"
menu-aria-label="More options menu"
>
<Menu :size="14" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
<!-- Icon with chevron -->
<VueMenu
size="x-sm"
menu-aria-label="More options menu"
>
<Menu :size="14" />
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h2>Monochrome Variant</h2>
</div>
<div class="stacked mbe4">
<VueMenu
menu-aria-label="Monochrome menu"
button-variant="monochrome"
>
Monochrome Menu
<template #menu>
<VueMenuItem
value="option1"
variant="monochrome"
>Option 1</VueMenuItem>
<VueMenuItem
value="option2"
variant="monochrome"
:selected="true"
>Option 2 (Selected)</VueMenuItem>
<VueMenuItem
value="option3"
variant="monochrome"
>Option 3</VueMenuItem>
</template>
</VueMenu>
</div>
<div class="mbe4">
<h2>Event Handling</h2>
</div>
<div class="stacked mbe4">
<div
class="flex-inline items-center"
:style="{gap: '10px'}"
>
<VueMenu
menu-aria-label="Event testing menu"
@menu-open="handleMenuOpen"
@menu-close="handleMenuClose"
@menu-select="handleMenuSelect"
>
Event Test
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
<p v-if="lastEvent">
Last event: <strong>{{ lastEvent }}</strong>
</p>
</div>
</div>
<div class="mbe4">
<h2>Menu Types: Navigation vs Selection</h2>
<p class="mbe4">
The <code>type</code> prop controls selection behavior. Use <code>type="default"</code> (the default) for navigation menus where selection is transient, and <code>type="single-select"</code> for menus where selection should persist (like filters or sorting).
</p>
</div>
<div class="stacked mbe4">
<div
class="flex items-start"
style="gap: 2rem; flex-wrap: wrap;"
>
<div>
<h3 class="mbe4">Navigation Menu (type="default")</h3>
<p class="mbe4">
Selection clears when menu closes. Use for navigation and actions.
</p>
<VueMenu menu-aria-label="User navigation">
User Menu
<template #menu>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
</div>
<div>
<h3 class="mbe4">Selection Menu (type="single-select")</h3>
<p class="mbe4">
Selection persists when menu closes. Use for filters, sorting, etc.
</p>
<VueMenu
menu-type="single-select"
selected-value="date"
menu-aria-label="Sort options"
>
Sort by
<template #menu>
<VueMenuItem value="date">Date</VueMenuItem>
<VueMenuItem value="name">Name</VueMenuItem>
<VueMenuItem value="size">Size</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
</div>
<div class="mbe4">
<h2>Additional Gutter</h2>
<p class="mbe4">
The <code>additional-gutter</code> prop adds extra vertical spacing beyond the trigger button's height when positioning the menu. This is useful when the menu button is within a taller container (like a header) and you need the menu to clear that container.
</p>
</div>
<div class="stacked mbe4">
<div>
<p class="mbe4">This menu has <code>additional-gutter="20px"</code></p>
<VueMenu
menu-variant="chevron"
additional-gutter="20px"
menu-aria-label="Menu with additional gutter"
>
Menu with Extra Gutter
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h2>Responsive Hidden Items (checkHiddenItems)</h2>
<p class="mbe4">
The <code>check-hidden-items</code> prop enables the menu to skip items that are hidden via CSS (like responsive media queries). This is useful when you wrap menu items in responsive containers but want keyboard navigation to work correctly.
</p>
<p class="mbe4">
<strong>Performance Note:</strong> Enabling this feature checks computed styles on every keyboard navigation, which has a performance cost. Only enable it if you're using CSS-based hiding. For better performance, prefer conditional rendering (v-if) instead.
</p>
</div>
<div class="stacked mbe4">
<div v-html="responsiveStyles"></div>
<div>
<p class="mbe4">
<strong>Try resizing your browser:</strong> Desktop items are hidden on mobile (<768px), mobile items are hidden on desktop. Keyboard navigation skips hidden items.
</p>
<VueMenu
check-hidden-items
menu-aria-label="Responsive menu"
>
Responsive Menu
<template #menu>
<div class="desktop-only-items">
<VueMenuItem value="desktop1">
<Monitor
:size="16"
class="mie2"
/>Desktop Item 1
</VueMenuItem>
<VueMenuItem value="desktop2">
<Monitor
:size="16"
class="mie2"
/>Desktop Item 2
</VueMenuItem>
</div>
<div class="mobile-only-items">
<VueMenuItem value="mobile1">
<Smartphone
:size="16"
class="mie2"
/>Mobile Item 1
</VueMenuItem>
<VueMenuItem value="mobile2">
<Smartphone
:size="16"
class="mie2"
/>Mobile Item 2
</VueMenuItem>
</div>
<VueMenuSeparator />
<VueMenuItem value="always">Always Visible</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h2>Recommended: Conditional Rendering (Better Performance)</h2>
<p class="mbe4">
Instead of using <code>check-hidden-items</code>, you can achieve the same result with better performance by using Vue's conditional rendering (v-if). This removes hidden items from the DOM entirely.
</p>
</div>
<div class="stacked mbe4">
<div>
<p class="mbe4">
<strong>Current viewport:</strong> {{ isMobile ? 'Mobile' : 'Desktop' }}
</p>
<VueMenu menu-aria-label="Conditional menu">
Conditional Menu
<template #menu>
<VueMenuItem
v-if="!isMobile"
value="desktop1"
>
<Monitor
:size="16"
class="mie2"
/>Desktop Item 1
</VueMenuItem>
<VueMenuItem
v-if="!isMobile"
value="desktop2"
>
<Monitor
:size="16"
class="mie2"
/>Desktop Item 2
</VueMenuItem>
<VueMenuItem
v-if="isMobile"
value="mobile1"
>
<Smartphone
:size="16"
class="mie2"
/>Mobile Item 1
</VueMenuItem>
<VueMenuItem
v-if="isMobile"
value="mobile2"
>
<Smartphone
:size="16"
class="mie2"
/>Mobile Item 2
</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="always">Always Visible</VueMenuItem>
</template>
</VueMenu>
</div>
</div>
<div class="mbe4">
<h2>Dynamic Icon Switching</h2>
<p class="mbe4">
The menu button exposes a <code>data-menu-open</code> attribute that changes based on the menu state. You can use this with CSS to dynamically switch icons when the menu opens/closes.
</p>
</div>
<div class="stacked mbe4">
<div v-html="dynamicIconStyles"></div>
<div
class="flex-inline items-center"
style="gap: 1rem;"
>
<VueMenu
menu-variant="icon"
ghost
size="sm"
class="dynamic-icon-menu"
aria-label="Toggle menu"
menu-aria-label="Navigation menu"
>
<Menu
:size="14"
class="menu-icon"
/>
<X
:size="14"
class="close-icon"
/>
<template #menu>
<VueMenuItem value="home">Home</VueMenuItem>
<VueMenuItem value="about">About</VueMenuItem>
<VueMenuItem value="contact">Contact</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
<p>
Click the icon to see it change from ☰ to ✕
</p>
</div>
</div>
<div class="mbe4">
<h2>CSS Shadow Parts Customization</h2>
</div>
<div class="stacked mbe4">
<div v-html="customMenuStyles"></div>
<VueMenu
menu-aria-label="Custom menu"
class="custom-menu-button"
>
Custom Menu
<template #menu>
<VueMenuItem value="one">Option 1</VueMenuItem>
<VueMenuItem value="two">Option 2</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="three">Option 3</VueMenuItem>
</template>
</VueMenu>
</div>
</section>
</template>
<script>
import { ref, onMounted, onUnmounted } from "vue";
import VueMenu, {
VueMenuItem,
VueMenuSeparator,
} from "agnosticui-core/menu/vue";
import { User, Menu, X, Monitor, Smartphone } from "lucide-vue-next";
export default {
name: "MenuExamples",
components: {
VueMenu,
VueMenuItem,
VueMenuSeparator,
User,
Menu,
X,
Monitor,
Smartphone,
},
setup() {
const lastEvent = ref(null);
const lastSelectedValue = ref(null);
const isMobile = ref(false);
const checkMobile = () => {
isMobile.value = window.innerWidth < 768;
};
onMounted(() => {
checkMobile();
window.addEventListener("resize", checkMobile);
});
onUnmounted(() => {
window.removeEventListener("resize", checkMobile);
});
const handleMenuOpen = (detail) => {
const selectedInfo = lastSelectedValue.value
? `, selected: ${lastSelectedValue.value}`
: "";
lastEvent.value = `menu-open (open: ${detail.open}${selectedInfo})`;
};
const handleMenuClose = (detail) => {
const selectedInfo = lastSelectedValue.value
? `, selected: ${lastSelectedValue.value}`
: "";
lastEvent.value = `menu-close (open: ${detail.open}${selectedInfo})`;
};
const handleMenuSelect = (detail) => {
lastSelectedValue.value = detail.value;
lastEvent.value = `menu-select (value: ${detail.value})`;
};
const responsiveStyles = `
<style>
/* Hide desktop items on mobile (< 768px) */
@media (max-width: 767px) {
.desktop-only-items {
display: none;
}
}
/* Hide mobile items on desktop (>= 768px) */
@media (min-width: 768px) {
.mobile-only-items {
display: none;
}
}
</style>
`;
const dynamicIconStyles = `
<style>
.dynamic-icon-menu .menu-icon,
.dynamic-icon-menu .close-icon {
transition: opacity var(--ag-motion-medium) ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] .close-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] .menu-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
</style>
`;
const customMenuStyles = `
<style>
.custom-menu-button ag-button::part(ag-button) {
background-color: #4a5568;
color: white;
border: 2px solid #2d3748;
border-radius: 8px;
}
.custom-menu-button .label {
font-weight: bold;
}
.custom-menu-button .chevron-icon {
color: #a0aec0;
}
.custom-menu-button ag-menu::part(ag-menu) {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
}
.custom-menu-button ag-menu::part(ag-menu-item) {
color: #e2e8f0;
}
.custom-menu-button ag-menu::part(ag-menu-item):hover {
background-color: #4a5568;
}
.custom-menu-button ag-menu::part(ag-menu-separator) {
background-color: #4a5568;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button) {
color: white;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button):focus,
.custom-menu-button ag-menu-item::part(ag-menu-item-button):hover {
color: black;
}
</style>
`;
return {
lastEvent,
lastSelectedValue,
isMobile,
handleMenuOpen,
handleMenuClose,
handleMenuSelect,
responsiveStyles,
dynamicIconStyles,
customMenuStyles,
};
},
};
</script>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/menu';
export class MenuLitExamples extends LitElement {
createRenderRoot() {
return this;
}
static get properties() {
return {
lastEvent: { type: String, state: true },
lastSelectedValue: { type: String, state: true },
isMobile: { type: Boolean, state: true },
};
}
constructor() {
super();
this.lastEvent = null;
this.lastSelectedValue = null;
this.isMobile = false;
}
connectedCallback() {
super.connectedCallback();
this.checkMobile();
window.addEventListener('resize', this.checkMobile.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this.checkMobile.bind(this));
}
checkMobile() {
this.isMobile = window.innerWidth < 768;
}
handleMenuOpen(e) {
const selectedInfo = this.lastSelectedValue ? `, selected: ${this.lastSelectedValue}` : '';
this.lastEvent = `menu-open (open: ${e.detail.open}${selectedInfo})`;
}
handleMenuClose(e) {
const selectedInfo = this.lastSelectedValue ? `, selected: ${this.lastSelectedValue}` : '';
this.lastEvent = `menu-close (open: ${e.detail.open}${selectedInfo})`;
}
handleMenuSelect(e) {
this.lastSelectedValue = e.detail.value;
this.lastEvent = `menu-select (value: ${e.detail.value})`;
}
render() {
return html`
<style>
/* Responsive styles */
@media (max-width: 767px) {
.desktop-only-items {
display: none;
}
}
@media (min-width: 768px) {
.mobile-only-items {
display: none;
}
}
/* Dynamic icon styles */
.dynamic-icon-menu .menu-icon,
.dynamic-icon-menu .close-icon {
transition: opacity var(--ag-motion-medium) ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] .close-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] .menu-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
/* Custom menu styles */
.custom-menu-button ag-button::part(ag-button) {
background-color: #4a5568;
color: white;
border: 2px solid #2d3748;
border-radius: 8px;
}
.custom-menu-button .label {
font-weight: bold;
}
.custom-menu-button .chevron-icon {
color: #a0aec0;
}
.custom-menu-button ag-menu::part(ag-menu) {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button) {
color: white;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button):focus,
.custom-menu-button ag-menu-item::part(ag-menu-item-button):hover {
color: black;
}
</style>
<section>
<div class="mbe4">
<h2>Basic Menu</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="Menu options">
Menu
<ag-menu slot="menu">
<ag-menu-item value="edit">Edit</ag-menu-item>
<ag-menu-item value="copy">Copy</ag-menu-item>
<ag-menu-item value="paste">Paste</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="delete">Delete</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Menu Alignment</h2>
<p class="mbe4">
The <code>menu-align</code> prop controls horizontal alignment. Use <code>menu-align="right"</code> when the menu button is near the right edge of the viewport.
</p>
</div>
<div class="stacked mbe4">
<div class="flex items-center" style="gap: 1rem; justify-content: space-between;">
<ag-menu-button menu-align="left" menu-aria-label="Left-aligned menu">
Left Aligned
<ag-menu slot="menu">
<ag-menu-item value="option1">Option 1</ag-menu-item>
<ag-menu-item value="option2">Option 2</ag-menu-item>
<ag-menu-item value="option3">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-align="right" menu-aria-label="Right-aligned menu">
Right Aligned
<ag-menu slot="menu">
<ag-menu-item value="option1">Option 1</ag-menu-item>
<ag-menu-item value="option2">Option 2</ag-menu-item>
<ag-menu-item value="option3">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h2>Menu with Links</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="Navigation menu">
Navigation
<ag-menu slot="menu">
<ag-menu-item value="home" href="#home">Home</ag-menu-item>
<ag-menu-item value="about" href="#about">About</ag-menu-item>
<ag-menu-item value="contact" href="#contact">Contact</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="external" href="https://example.com" target="_blank">
External Link
</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Disabled Button</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button disabled menu-aria-label="Disabled menu">
Disabled Menu
<ag-menu slot="menu">
<ag-menu-item value="item1">Item 1</ag-menu-item>
<ag-menu-item value="item2">Item 2</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Menu with Disabled Items</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="Menu with disabled items">
Mixed States
<ag-menu slot="menu">
<ag-menu-item value="enabled1">Enabled Item</ag-menu-item>
<ag-menu-item value="disabled1" disabled>Disabled Item</ag-menu-item>
<ag-menu-item value="enabled2">Another Enabled</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="disabled2" disabled>Another Disabled</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Complex Menu (File Menu)</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="File menu">
File
<ag-menu slot="menu">
<ag-menu-item value="new">New</ag-menu-item>
<ag-menu-item value="open">Open...</ag-menu-item>
<ag-menu-item value="recent">Open Recent</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="save">Save</ag-menu-item>
<ag-menu-item value="save-as">Save As...</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="export">Export</ag-menu-item>
<ag-menu-item value="import">Import</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="print">Print</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="close">Close</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Menu with Sections</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="User menu">
<div class="flex-inline items-center">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
User Menu
</div>
<ag-menu slot="menu">
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="help">Help</ag-menu-item>
<ag-menu-item value="feedback">Send Feedback</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Menu with Icon Button</h2>
<p class="mbe4">
The following examples show using an icon as the trigger. The <code>menu-variant="icon"</code> removes the chevron and provides a minimal button container for the icon. You can also use an icon with the default chevron trigger as we see on the right.
</p>
</div>
<div class="mbe4">
<h3>Large</h3>
<div class="flex-inline items-center" style="gap: 1rem;">
<!-- Icon only -->
<ag-menu-button menu-variant="icon" ghost menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
<!-- Icon with chevron -->
<ag-menu-button menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h3>Medium</h3>
<div class="flex-inline items-center" style="gap: 1rem;">
<!-- Icon only -->
<ag-menu-button menu-variant="icon" ghost size="sm" menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
<!-- Icon with chevron -->
<ag-menu-button size="sm" menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h3>Small</h3>
<div class="flex-inline items-center" style="gap: 1rem;">
<!-- Icon only -->
<ag-menu-button menu-variant="icon" ghost size="x-sm" menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
<!-- Icon with chevron -->
<ag-menu-button size="x-sm" menu-aria-label="More options menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<ag-menu slot="menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h2>Monochrome Variant</h2>
</div>
<div class="stacked mbe4">
<ag-menu-button menu-aria-label="Monochrome menu" button-variant="monochrome">
Monochrome Menu
<ag-menu slot="menu">
<ag-menu-item value="option1" variant="monochrome">Option 1</ag-menu-item>
<ag-menu-item value="option2" variant="monochrome" selected>Option 2 (Selected)</ag-menu-item>
<ag-menu-item value="option3" variant="monochrome">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div class="mbe4">
<h2>Event Handling</h2>
</div>
<div class="stacked mbe4">
<div class="flex-inline items-center" style="gap: 10px;">
<ag-menu-button
menu-aria-label="Event testing menu"
@menu-open=${this.handleMenuOpen}
@menu-close=${this.handleMenuClose}
@menu-select=${this.handleMenuSelect}
>
Event Test
<ag-menu slot="menu">
<ag-menu-item value="option1">Option 1</ag-menu-item>
<ag-menu-item value="option2">Option 2</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="option3">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
${this.lastEvent ? html`<p>Last event: <strong>${this.lastEvent}</strong></p>` : ''}
</div>
</div>
<div class="mbe4">
<h2>Menu Types: Navigation vs Selection</h2>
<p class="mbe4">
The <code>type</code> prop controls selection behavior. Use <code>type="default"</code> (the default) for navigation menus where selection is transient, and <code>type="single-select"</code> for menus where selection should persist (like filters or sorting).
</p>
</div>
<div class="stacked mbe4">
<div class="flex items-start" style="gap: 2rem; flex-wrap: wrap;">
<div>
<h3 class="mbe4">Navigation Menu (type="default")</h3>
<p class="mbe4">
Selection clears when menu closes. Use for navigation and actions.
</p>
<ag-menu-button menu-aria-label="User navigation">
User Menu
<ag-menu slot="menu">
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
<div>
<h3 class="mbe4">Selection Menu (type="single-select")</h3>
<p class="mbe4">
Selection persists when menu closes. Use for filters, sorting, etc.
</p>
<ag-menu-button menu-type="single-select" selected-value="date" menu-aria-label="Sort options">
Sort by
<ag-menu slot="menu">
<ag-menu-item value="date">Date</ag-menu-item>
<ag-menu-item value="name">Name</ag-menu-item>
<ag-menu-item value="size">Size</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
</div>
<div class="mbe4">
<h2>Additional Gutter</h2>
<p class="mbe4">
The <code>additional-gutter</code> prop adds extra vertical spacing beyond the trigger button's height when positioning the menu. This is useful when the menu button is within a taller container (like a header) and you need the menu to clear that container.
</p>
</div>
<div class="stacked mbe4">
<div>
<p class="mbe4">This menu has <code>additional-gutter="20px"</code></p>
<ag-menu-button menu-variant="chevron" additional-gutter="20px" menu-aria-label="Menu with additional gutter">
Menu with Extra Gutter
<ag-menu slot="menu">
<ag-menu-item value="option1">Option 1</ag-menu-item>
<ag-menu-item value="option2">Option 2</ag-menu-item>
<ag-menu-item value="option3">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h2>Responsive Hidden Items (checkHiddenItems)</h2>
<p class="mbe4">
The <code>check-hidden-items</code> prop enables the menu to skip items that are hidden via CSS (like responsive media queries). This is useful when you wrap menu items in responsive containers but want keyboard navigation to work correctly.
</p>
<p class="mbe4">
<strong>Performance Note:</strong> Enabling this feature checks computed styles on every keyboard navigation, which has a performance cost. Only enable it if you're using CSS-based hiding. For better performance, prefer conditional rendering instead.
</p>
</div>
<div class="stacked mbe4">
<div>
<p class="mbe4">
<strong>Try resizing your browser:</strong> Desktop items are hidden on mobile (<768px), mobile items are hidden on desktop. Keyboard navigation skips hidden items.
</p>
<ag-menu-button check-hidden-items menu-aria-label="Responsive menu">
Responsive Menu
<ag-menu slot="menu">
<div class="desktop-only-items">
<ag-menu-item value="desktop1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>Desktop Item 1
</ag-menu-item>
<ag-menu-item value="desktop2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>Desktop Item 2
</ag-menu-item>
</div>
<div class="mobile-only-items">
<ag-menu-item value="mobile1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>Mobile Item 1
</ag-menu-item>
<ag-menu-item value="mobile2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>Mobile Item 2
</ag-menu-item>
</div>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="always">Always Visible</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
<div class="mbe4">
<h2>Recommended: Conditional Rendering (Better Performance)</h2>
<p class="mbe4">
Instead of using <code>check-hidden-items</code>, you can achieve the same result with better performance by using conditional rendering. This removes hidden items from the DOM entirely.
</p>
</div>
<div class="stacked mbe4">
<div>
<p class="mbe4">
<strong>Current viewport:</strong> ${this.isMobile ? 'Mobile' : 'Desktop'}
</p>
<ag-menu-button menu-aria-label="Conditional menu">
Conditional Menu
<ag-menu slot="menu">
${!this.isMobile ? html`
<ag-menu-item value="desktop1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>Desktop Item 1
</ag-menu-item>
<ag-menu-item value="desktop2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>Desktop Item 2
</ag-menu-item>
` : ''}
${this.isMobile ? html`
<ag-menu-item value="mobile1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>Mobile Item 1
</ag-menu-item>
<ag-menu-item value="mobile2">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" class="mie2" style="display: inline-block; vertical-align: middle;">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<line x1="12" y1="18" x2="12.01" y2="18"/>
</svg>Mobile Item 2
</ag-menu-item>
` : ''}
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="always">Always Visible</ag-menu-item>
</ag-menu>
</ag-menu-button>
</div>
</div>
</section>
`;
}
}
// Register the custom element
customElements.define('menu-lit-examples', MenuLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState, useEffect } from "react";
import {
ReactMenuButton,
ReactMenu,
ReactMenuItem,
ReactMenuSeparator,
} from "agnosticui-core/menu/react";
export default function MenuReactExamples() {
const [lastEvent, setLastEvent] = useState(null);
const [lastSelectedValue, setLastSelectedValue] = useState(null);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener("resize", checkMobile);
return () => window.removeEventListener("resize", checkMobile);
}, []);
const handleMenuOpen = (e) => {
const selectedInfo = lastSelectedValue
? `, selected: ${lastSelectedValue}`
: "";
setLastEvent(`menu-open (open: ${e.detail.open}${selectedInfo})`);
};
const handleMenuClose = (e) => {
const selectedInfo = lastSelectedValue
? `, selected: ${lastSelectedValue}`
: "";
setLastEvent(`menu-close (open: ${e.detail.open}${selectedInfo})`);
};
const handleMenuSelect = (e) => {
setLastSelectedValue(e.detail.value);
setLastEvent(`menu-select (value: ${e.detail.value})`);
};
return (
<>
<style>{`
/* Responsive styles */
@media (max-width: 767px) {
.desktop-only-items {
display: none;
}
}
@media (min-width: 768px) {
.mobile-only-items {
display: none;
}
}
/* Dynamic icon styles */
.dynamic-icon-menu .menu-icon,
.dynamic-icon-menu .close-icon {
transition: opacity var(--ag-motion-medium) ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] .close-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] .menu-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
/* Custom menu styles */
.custom-menu-button ag-button::part(ag-button) {
background-color: #4a5568;
color: white;
border: 2px solid #2d3748;
border-radius: 8px;
}
.custom-menu-button .label {
font-weight: bold;
}
.custom-menu-button .chevron-icon {
color: #a0aec0;
}
.custom-menu-button ag-menu::part(ag-menu) {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button) {
color: white;
}
.custom-menu-button ag-menu-item::part(ag-menu-item-button):focus,
.custom-menu-button ag-menu-item::part(ag-menu-item-button):hover {
color: black;
}
`}</style>
<section>
<div className="mbe4">
<h2>Basic Menu</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton menuAriaLabel="Menu options">
Menu
<ReactMenu slot="menu">
<ReactMenuItem value="edit">Edit</ReactMenuItem>
<ReactMenuItem value="copy">Copy</ReactMenuItem>
<ReactMenuItem value="paste">Paste</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="delete">Delete</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Menu Alignment</h2>
<p className="mbe4">
The <code>menu-align</code> prop controls horizontal alignment. Use{" "}
<code>menu-align="right"</code> when the menu button is near the
right edge of the viewport.
</p>
</div>
<div className="stacked mbe4">
<div
className="flex items-center"
style={{ gap: "1rem", justifyContent: "space-between" }}
>
<ReactMenuButton menuAlign="left" menuAriaLabel="Left-aligned menu">
Left Aligned
<ReactMenu slot="menu">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuAlign="right"
menuAriaLabel="Right-aligned menu"
>
Right Aligned
<ReactMenu slot="menu">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h2>Menu with Links</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton menuAriaLabel="Navigation menu">
Navigation
<ReactMenu slot="menu">
<ReactMenuItem value="home" href="#home">
Home
</ReactMenuItem>
<ReactMenuItem value="about" href="#about">
About
</ReactMenuItem>
<ReactMenuItem value="contact" href="#contact">
Contact
</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem
value="external"
href="https://example.com"
target="_blank"
>
External Link
</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Disabled Button</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton disabled menuAriaLabel="Disabled menu">
Disabled Menu
<ReactMenu slot="menu">
<ReactMenuItem value="item1">Item 1</ReactMenuItem>
<ReactMenuItem value="item2">Item 2</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Menu with Disabled Items</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton menuAriaLabel="Menu with disabled items">
Mixed States
<ReactMenu slot="menu">
<ReactMenuItem value="enabled1">Enabled Item</ReactMenuItem>
<ReactMenuItem value="disabled1" disabled>
Disabled Item
</ReactMenuItem>
<ReactMenuItem value="enabled2">Another Enabled</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="disabled2" disabled>
Another Disabled
</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Complex Menu (File Menu)</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton menuAriaLabel="File menu">
File
<ReactMenu slot="menu">
<ReactMenuItem value="new">New</ReactMenuItem>
<ReactMenuItem value="open">Open...</ReactMenuItem>
<ReactMenuItem value="recent">Open Recent</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="save">Save</ReactMenuItem>
<ReactMenuItem value="save-as">Save As...</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="export">Export</ReactMenuItem>
<ReactMenuItem value="import">Import</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="print">Print</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="close">Close</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Menu with Sections</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton menuAriaLabel="User menu">
<div className="flex-inline items-center">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
User Menu
</div>
<ReactMenu slot="menu">
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="help">Help</ReactMenuItem>
<ReactMenuItem value="feedback">Send Feedback</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Menu with Icon Button</h2>
<p className="mbe4">
The following examples show using an icon as the trigger. The{" "}
<code>menu-variant="icon"</code> removes the chevron and provides a
minimal button container for the icon. You can also use an icon with
the default chevron trigger as we see on the right.
</p>
</div>
<div className="mbe4">
<h3>Large</h3>
<div className="flex-inline items-center" style={{ gap: "1rem" }}>
{/* Icon only */}
<ReactMenuButton
menuVariant="icon"
ghost
menuAriaLabel="More options menu"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="24"
height="24"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
{/* Icon with chevron */}
<ReactMenuButton menuAriaLabel="More options menu">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="24"
height="24"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h3>Medium</h3>
<div className="flex-inline items-center" style={{ gap: "1rem" }}>
{/* Icon only */}
<ReactMenuButton
menuVariant="icon"
ghost
size="sm"
menuAriaLabel="More options menu"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="18"
height="18"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
{/* Icon with chevron */}
<ReactMenuButton size="sm" menuAriaLabel="More options menu">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="18"
height="18"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h3>Small</h3>
<div className="flex-inline items-center" style={{ gap: "1rem" }}>
{/* Icon only */}
<ReactMenuButton
menuVariant="icon"
ghost
size="x-sm"
menuAriaLabel="More options menu"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="14"
height="14"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
{/* Icon with chevron */}
<ReactMenuButton size="x-sm" menuAriaLabel="More options menu">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="14"
height="14"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h2>Monochrome Variant</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton
menuAriaLabel="Monochrome menu"
buttonVariant="monochrome"
>
Monochrome Menu
<ReactMenu slot="menu">
<ReactMenuItem value="option1" variant="monochrome">
Option 1
</ReactMenuItem>
<ReactMenuItem value="option2" variant="monochrome" selected>
Option 2 (Selected)
</ReactMenuItem>
<ReactMenuItem value="option3" variant="monochrome">
Option 3
</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div className="mbe4">
<h2>Event Handling</h2>
</div>
<div className="stacked mbe4">
<div className="flex-inline items-center" style={{ gap: "10px" }}>
<ReactMenuButton
menuAriaLabel="Event testing menu"
onMenuOpen={handleMenuOpen}
onMenuClose={handleMenuClose}
onMenuSelect={handleMenuSelect}
>
Event Test
<ReactMenu slot="menu">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
{lastEvent && (
<p>
Last event: <strong>{lastEvent}</strong>
</p>
)}
</div>
</div>
<div className="mbe4">
<h2>Menu Types: Navigation vs Selection</h2>
<p className="mbe4">
The <code>type</code> prop controls selection behavior. Use{" "}
<code>type="default"</code> (the default) for navigation menus where
selection is transient, and <code>type="single-select"</code> for
menus where selection should persist (like filters or sorting).
</p>
</div>
<div className="stacked mbe4">
<div
className="flex items-start"
style={{ gap: "2rem", flexWrap: "wrap" }}
>
<div>
<h3 className="mbe4">Navigation Menu (type="default")</h3>
<p className="mbe4">
Selection clears when menu closes. Use for navigation and
actions.
</p>
<ReactMenuButton menuAriaLabel="User navigation">
User Menu
<ReactMenu slot="menu">
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
<div>
<h3 className="mbe4">Selection Menu (type="single-select")</h3>
<p className="mbe4">
Selection persists when menu closes. Use for filters, sorting,
etc.
</p>
<ReactMenuButton
menuType="single-select"
selectedValue="date"
menuAriaLabel="Sort options"
>
Sort by
<ReactMenu slot="menu">
<ReactMenuItem value="date">Date</ReactMenuItem>
<ReactMenuItem value="name">Name</ReactMenuItem>
<ReactMenuItem value="size">Size</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
</div>
<div className="mbe4">
<h2>Additional Gutter</h2>
<p className="mbe4">
The <code>additional-gutter</code> prop adds extra vertical spacing
beyond the trigger button's height when positioning the menu. This
is useful when the menu button is within a taller container (like a
header) and you need the menu to clear that container.
</p>
</div>
<div className="stacked mbe4">
<div>
<p className="mbe4">
This menu has <code>additional-gutter="20px"</code>
</p>
<ReactMenuButton
menuVariant="chevron"
additionalGutter="20px"
menuAriaLabel="Menu with additional gutter"
>
Menu with Extra Gutter
<ReactMenu slot="menu">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h2>Responsive Hidden Items (checkHiddenItems)</h2>
<p className="mbe4">
The <code>check-hidden-items</code> prop enables the menu to skip
items that are hidden via CSS (like responsive media queries). This
is useful when you wrap menu items in responsive containers but want
keyboard navigation to work correctly.
</p>
<p className="mbe4">
<strong>Performance Note:</strong> Enabling this feature checks
computed styles on every keyboard navigation, which has a
performance cost. Only enable it if you're using CSS-based hiding.
For better performance, prefer conditional rendering instead.
</p>
</div>
<div className="stacked mbe4">
<div>
<p className="mbe4">
<strong>Try resizing your browser:</strong> Desktop items are
hidden on mobile (<768px), mobile items are hidden on desktop.
Keyboard navigation skips hidden items.
</p>
<ReactMenuButton checkHiddenItems menuAriaLabel="Responsive menu">
Responsive Menu
<ReactMenu slot="menu">
<div className="desktop-only-items">
<ReactMenuItem value="desktop1">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
Desktop Item 1
</ReactMenuItem>
<ReactMenuItem value="desktop2">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
Desktop Item 2
</ReactMenuItem>
</div>
<div className="mobile-only-items">
<ReactMenuItem value="mobile1">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</svg>
Mobile Item 1
</ReactMenuItem>
<ReactMenuItem value="mobile2">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</svg>
Mobile Item 2
</ReactMenuItem>
</div>
<ReactMenuSeparator />
<ReactMenuItem value="always">Always Visible</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h2>Recommended: Conditional Rendering (Better Performance)</h2>
<p className="mbe4">
Instead of using <code>check-hidden-items</code>, you can achieve
the same result with better performance by using conditional
rendering. This removes hidden items from the DOM entirely.
</p>
</div>
<div className="stacked mbe4">
<div>
<p className="mbe4">
<strong>Current viewport:</strong>{" "}
{isMobile ? "Mobile" : "Desktop"}
</p>
<ReactMenuButton menuAriaLabel="Conditional menu">
Conditional Menu
<ReactMenu slot="menu">
{!isMobile && (
<ReactMenuItem value="desktop1">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
Desktop Item 1
</ReactMenuItem>
)}
{!isMobile && (
<ReactMenuItem value="desktop2">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
Desktop Item 2
</ReactMenuItem>
)}
{isMobile && (
<ReactMenuItem value="mobile1">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</svg>
Mobile Item 1
</ReactMenuItem>
)}
{isMobile && (
<ReactMenuItem value="mobile2">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="16"
height="16"
className="mie2"
style={{
display: "inline-block",
verticalAlign: "middle",
}}
>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
<line x1="12" y1="18" x2="12.01" y2="18" />
</svg>
Mobile Item 2
</ReactMenuItem>
)}
<ReactMenuSeparator />
<ReactMenuItem value="always">Always Visible</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</div>
<div className="mbe4">
<h2>Dynamic Icon Switching</h2>
<p className="mbe4">
The menu button exposes a <code>data-menu-open</code> attribute that
changes based on the menu state. You can use this with CSS to
dynamically switch icons when the menu opens/closes.
</p>
</div>
<div className="stacked mbe4">
<div className="flex-inline items-center" style={{ gap: "1rem" }}>
<ReactMenuButton
menuVariant="icon"
ghost
size="sm"
className="dynamic-icon-menu"
ariaLabel="Toggle menu"
menuAriaLabel="Navigation menu"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="14"
height="14"
className="menu-icon"
>
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width="14"
height="14"
className="close-icon"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<ReactMenu slot="menu">
<ReactMenuItem value="home">Home</ReactMenuItem>
<ReactMenuItem value="about">About</ReactMenuItem>
<ReactMenuItem value="contact">Contact</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<p>Click the icon to see it change from ☰ to ✕</p>
</div>
</div>
<div className="mbe4">
<h2>CSS Shadow Parts Customization</h2>
</div>
<div className="stacked mbe4">
<ReactMenuButton
menuAriaLabel="Custom menu"
className="custom-menu-button"
>
Custom Menu
<ReactMenu slot="menu">
<ReactMenuItem value="one">Option 1</ReactMenuItem>
<ReactMenuItem value="two">Option 2</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="three">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</div>
</section>
</>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add MenuThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<section>
<VueMenu
menu-variant="chevron"
menu-aria-label="Menu options"
>
Menu
<template #menu>
<VueMenuItem value="edit">Edit</VueMenuItem>
<VueMenuItem value="copy">Copy</VueMenuItem>
<VueMenuItem value="paste">Paste</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="delete">Delete</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-variant="button"
button-variant="primary"
size="md"
menu-aria-label="Actions menu"
>
Actions
<template #menu>
<VueMenuItem value="new">New File</VueMenuItem>
<VueMenuItem value="open">Open File</VueMenuItem>
<VueMenuItem value="save">Save</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-variant="icon"
ghost
unicode="⋮"
aria-label="More options"
menu-aria-label="More options menu"
>
<template #menu>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuSeparator />
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-align="right"
menu-aria-label="Right-aligned menu"
>
Options
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
<VueMenu menu-aria-label="Navigation menu">
Navigation
<template #menu>
<VueMenuItem value="home" href="#home">Home</VueMenuItem>
<VueMenuItem value="about" href="#about">About</VueMenuItem>
<VueMenuItem value="contact" href="#contact">Contact</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-variant="button"
button-variant="primary"
menu-aria-label="Menu with disabled items"
>
Mixed States
<template #menu>
<VueMenuItem value="enabled1">Enabled Item</VueMenuItem>
<VueMenuItem value="disabled1" :disabled="true">Disabled Item</VueMenuItem>
<VueMenuItem value="enabled2">Another Enabled</VueMenuItem>
</template>
</VueMenu>
<VueMenu
menu-aria-label="Event testing menu"
@menu-open="handleMenuOpen"
@menu-close="handleMenuClose"
@menu-select="handleMenuSelect"
>
Event Test
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
</template>
</VueMenu>
</section>
</template>
<script>
import VueMenu, { VueMenuItem, VueMenuSeparator } from "agnosticui-core/menu/vue";
export default {
components: {
VueMenu,
VueMenuItem,
VueMenuSeparator,
},
methods: {
handleMenuOpen(detail) {
console.log("Menu opened, open:", detail.open);
},
handleMenuClose(detail) {
console.log("Menu closed, open:", detail.open);
},
handleMenuSelect(detail) {
console.log("Selected:", detail.value);
},
},
};
</script>React
import { ReactMenuButton, ReactMenu, ReactMenuItem, ReactMenuSeparator } from 'agnosticui-core/menu/react';
export default function MenuExample() {
const handleMenuOpen = (event) => {
console.log("Menu opened, open:", event.detail.open);
};
const handleMenuClose = (event) => {
console.log("Menu closed, open:", event.detail.open);
};
const handleMenuSelect = (event) => {
console.log("Selected:", event.detail.value);
};
return (
<section>
<ReactMenuButton
menuVariant="chevron"
size="md"
onMenuOpen={handleMenuOpen}
onMenuClose={handleMenuClose}
>
Menu
<ReactMenu slot="menu" ariaLabel="Menu options">
<ReactMenuItem value="edit" onMenuSelect={handleMenuSelect}>
Edit
</ReactMenuItem>
<ReactMenuItem value="copy" onMenuSelect={handleMenuSelect}>
Copy
</ReactMenuItem>
<ReactMenuItem value="paste" onMenuSelect={handleMenuSelect}>
Paste
</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="delete" onMenuSelect={handleMenuSelect}>
Delete
</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuVariant="button"
size="md"
buttonVariant="primary"
>
Actions
<ReactMenu slot="menu" ariaLabel="Action menu">
<ReactMenuItem value="new">New File</ReactMenuItem>
<ReactMenuItem value="open">Open File</ReactMenuItem>
<ReactMenuItem value="save">Save</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuVariant="icon"
size="md"
ghost
unicode="⋮"
ariaLabel="More options"
>
<ReactMenu slot="menu" ariaLabel="More options menu">
<ReactMenuItem value="settings">Settings</ReactMenuItem>
<ReactMenuItem value="profile">Profile</ReactMenuItem>
<ReactMenuSeparator />
<ReactMenuItem value="logout">Logout</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuVariant="chevron"
size="md"
menuAlign="right"
>
Options
<ReactMenu slot="menu" ariaLabel="Right-aligned menu">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuVariant="chevron"
size="md"
>
Navigation
<ReactMenu slot="menu" ariaLabel="Navigation menu">
<ReactMenuItem value="home" href="#home">Home</ReactMenuItem>
<ReactMenuItem value="about" href="#about">About</ReactMenuItem>
<ReactMenuItem value="contact" href="#contact">Contact</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
<ReactMenuButton
menuVariant="button"
size="md"
buttonVariant="primary"
>
Mixed States
<ReactMenu slot="menu" ariaLabel="Menu with disabled items">
<ReactMenuItem value="enabled1">Enabled Item</ReactMenuItem>
<ReactMenuItem value="disabled1" disabled>Disabled Item</ReactMenuItem>
<ReactMenuItem value="enabled2">Another Enabled</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</section>
);
}Lit (Web Components)
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import 'agnosticui-core/menu';
@customElement('menu-example')
export class MenuExample extends LitElement {
static styles = css`
:host {
display: block;
}
section {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
`;
firstUpdated() {
// Set up event listeners for menu components in the shadow DOM
const menuButton = this.shadowRoot?.querySelector('#my-menu');
menuButton?.addEventListener('menu-open', (e: Event) => {
const customEvent = e as CustomEvent;
console.log('Menu opened, open:', customEvent.detail.open);
});
menuButton?.addEventListener('menu-close', (e: Event) => {
const customEvent = e as CustomEvent;
console.log('Menu closed, open:', customEvent.detail.open);
});
const menuItems = this.shadowRoot?.querySelectorAll('ag-menu-item');
menuItems?.forEach((item) => {
item.addEventListener('menu-select', (e: Event) => {
const customEvent = e as CustomEvent;
console.log('Selected:', customEvent.detail.value);
});
});
const menuButton2 = this.shadowRoot?.querySelector('#callback-menu') as any;
if (menuButton2) {
menuButton2.onMenuOpen = (e: CustomEvent) => {
console.log('Menu opened via callback, open:', e.detail.open);
};
menuButton2.onMenuClose = (e: CustomEvent) => {
console.log('Menu closed via callback, open:', e.detail.open);
};
}
}
render() {
return html`
<section>
<ag-menu-button id="my-menu" menu-variant="chevron" size="md">
Menu
<ag-menu slot="menu" aria-label="Menu options">
<ag-menu-item value="edit">Edit</ag-menu-item>
<ag-menu-item value="copy">Copy</ag-menu-item>
<ag-menu-item value="paste">Paste</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="delete">Delete</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-variant="button" size="md" button-variant="primary">
Actions
<ag-menu slot="menu" aria-label="Action menu">
<ag-menu-item value="new">New File</ag-menu-item>
<ag-menu-item value="open">Open File</ag-menu-item>
<ag-menu-item value="save">Save</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="exit">Exit</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-variant="icon" size="md" ghost unicode="⋮" aria-label="More options">
<ag-menu slot="menu" aria-label="More options menu">
<ag-menu-item value="settings">Settings</ag-menu-item>
<ag-menu-item value="profile">Profile</ag-menu-item>
<ag-menu-separator></ag-menu-separator>
<ag-menu-item value="logout">Logout</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-variant="chevron" size="md" menu-align="right">
Options
<ag-menu slot="menu" aria-label="Right-aligned menu">
<ag-menu-item value="option1">Option 1</ag-menu-item>
<ag-menu-item value="option2">Option 2</ag-menu-item>
<ag-menu-item value="option3">Option 3</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-variant="chevron" size="md">
Navigation
<ag-menu slot="menu" aria-label="Navigation menu">
<ag-menu-item value="home" href="#home">Home</ag-menu-item>
<ag-menu-item value="about" href="#about">About</ag-menu-item>
<ag-menu-item value="contact" href="#contact">Contact</ag-menu-item>
</ag-menu>
</ag-menu-button>
<ag-menu-button menu-variant="button" size="md" button-variant="primary">
Mixed States
<ag-menu slot="menu" aria-label="Menu with disabled items">
<ag-menu-item value="enabled1">Enabled Item</ag-menu-item>
<ag-menu-item value="disabled1" disabled>Disabled Item</ag-menu-item>
<ag-menu-item value="enabled2">Another Enabled</ag-menu-item>
</ag-menu>
</ag-menu-button>
</section>
`;
}
}Note: When using menu components within a custom element's shadow DOM, set up event listeners in the component's lifecycle (e.g., firstUpdated()) rather than using DOMContentLoaded, as document.querySelector() cannot access elements inside shadow DOM. Use this.shadowRoot.querySelector() instead.
Props
Menu Button (React/Lit)
| Prop | Type | Default | Description |
|---|---|---|---|
menuVariant | 'chevron' | 'button' | 'icon' | 'chevron' | Structural variant of the menu button |
menuAlign | 'left' | 'right' | 'left' | Horizontal alignment of the menu relative to the button |
buttonVariant | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'monochrome' | '' | '' | Color variant from AgButton |
size | 'x-sm' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Size of the menu button (from AgButton) |
shape | 'capsule' | 'rounded' | 'circle' | 'square' | 'rounded-square' | '' | 'rounded' | Shape of the button (from AgButton) |
bordered | boolean | false | Use bordered button style (from AgButton) |
ghost | boolean | false | Use ghost button style (from AgButton) |
link | boolean | false | Use link button style (from AgButton) |
grouped | boolean | false | Part of a button group (from AgButton) |
disabled | boolean | false | Disables the menu button |
loading | boolean | false | Show loading state (from AgButton) |
ariaLabel | string | '' | ARIA label for the menu button |
ariaDescribedby | string | '' | ARIA describedby for the menu button |
unicode | string | '' | Unicode character for icon variant |
additionalGutter | string | '' | Additional vertical spacing beyond the trigger button height when positioning the menu (e.g., '10px', '1rem') |
Menu (Vue)
VueMenu inherits all the properties from Menu Button above, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Whether the menu is open (controlled mode) |
placement | 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom-start' | Placement of the menu popover |
menuAriaLabel | string | '' | ARIA label for the menu |
menuAriaLabelledBy | string | '' | ARIA labelledby for the menu |
Menu (ag-menu element)
These props apply to the <ag-menu> element itself (nested inside the button):
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'default' | 'single-select' | 'default' | Selection behavior: 'default' for navigation (transient selection), 'single-select' for persistent selection |
selectedValue | string | undefined | The currently selected item value (only applies to type="single-select") |
ariaLabel | string | '' | ARIA label for the menu |
ariaLabelledBy | string | '' | ARIA labelledby for the menu |
Menu Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | '' | Unique value for the menu item |
disabled | boolean | false | Disables the menu item |
href | string | '' | URL for menu item link |
target | string | '' | Target for menu item link (e.g., _blank) |
checked | boolean | false | Whether the menu item is checked (for single-select menus) |
variant | 'default' | 'monochrome' | 'default' | Visual variant of the menu item |
Events
MenuButton Events
| Event | Framework | Detail | Description |
|---|---|---|---|
menu-open | Vue: @menu-openReact: onMenuOpenLit: @menu-open or .onMenuOpen | { open: boolean } | Fired when menu opens. The open property will be true. |
menu-close | Vue: @menu-closeReact: onMenuCloseLit: @menu-close or .onMenuClose | { open: boolean } | Fired when menu closes. The open property will be false. |
click | Vue: @clickReact: onClickLit: @click or .onClick | Native MouseEvent | Fired when the menu button is clicked (native composed event). |
focus | Vue: @focusReact: onFocusLit: @focus or .onFocus | Native FocusEvent | Fired when the menu button receives focus (re-dispatched from host). |
blur | Vue: @blurReact: onBlurLit: @blur or .onBlur | Native FocusEvent | Fired when the menu button loses focus (re-dispatched from host). |
keydown | Vue: @keydownReact: onKeyDownLit: @keydown or .onKeyDown | Native KeyboardEvent | Fired when a key is pressed on the menu button (native composed event). |
MenuItem Events
| Event | Framework | Detail | Description |
|---|---|---|---|
menu-select | Vue: @menu-selectReact: onMenuSelectLit: @menu-select or .onMenuSelect | { value: string } | Fired when a menu item is selected. Contains the item's value. |
click | Vue: @clickReact: onClickLit: @click or .onClick | Native MouseEvent | Fired when the menu item is clicked (native composed event). |
Menu Events
| Event | Framework | Detail | Description |
|---|---|---|---|
keydown | Vue: @menu-keydownReact: onKeyDown on menuLit: @keydown or .onKeyDown | Native KeyboardEvent | Fired when a key is pressed in the menu (native composed event). |
Event Patterns
AgnosticUI Menu supports three event handling patterns:
- addEventListener (Lit/Vanilla JS):
const menuButton = document.querySelector("ag-menu-button");
menuButton.addEventListener("menu-open", (e) => {
console.log("Menu opened:", e.detail.open);
});- Callback props (Lit/Vanilla JS):
const menuButton = document.querySelector("ag-menu-button");
menuButton.onMenuOpen = (e) => {
console.log("Menu opened:", e.detail.open);
};- Framework event handlers (Vue/React):
<VueMenu @menu-open="handleMenuOpen" /><ReactMenuButton onMenuOpen={handleMenuOpen} />All three patterns work identically thanks to the dual-dispatch system.
Components
Vue
- VueMenu: Main menu component with button and popover
- VueMenuItem: Individual menu item (button or link)
- VueMenuSeparator: Visual separator between menu items
React
- ReactMenuButton: Menu trigger button
- ReactMenu: Menu popover container
- ReactMenuItem: Individual menu item (button or link)
- ReactMenuSeparator: Visual separator between menu items
Lit
- ag-menu-button: Menu trigger button
- ag-menu: Menu popover container
- ag-menu-item: Individual menu item (button or link)
- ag-menu-separator: Visual separator between menu items
Slots
Vue
- Default slot: Content for the menu button
- #menu template slot: Menu items
React
- children: Content for the menu button
- ReactMenu with slot="menu": Menu items
Lit
- Default slot: Content for the menu button
- ag-menu with slot="menu": Menu items
Accessibility
The Menu component implements the WAI-ARIA Menu Button Pattern:
- Uses
role="button"andaria-haspopup="true"for the menu trigger - Uses
role="menu"for the menu container - Uses
role="menuitem"for individual menu items - Implements keyboard navigation:
- Enter/Space: Opens the menu when focused on button
- Arrow Up/Down: Navigates between menu items
- Home/End: Jumps to first/last menu item
- Enter: Selects the focused menu item
- Escape/Tab: Closes the menu
- Manages focus automatically:
- Focus moves to first item when menu opens
- Focus returns to button when menu closes
- Disabled items are skipped during navigation
- Supports
aria-labelfor accessibility - Menu items can be marked with
disabledattribute
Best Practices
- Always provide an
aria-labelormenu-aria-labelfor accessibility - Use semantic grouping with
MenuSeparatorto organize related items - Prefer menu items over buttons when presenting a list of actions
- Use the
hrefprop for navigation items instead of click handlers - Keep menu item labels concise and action-oriented
- Use the
disabledprop rather than hiding items when possible - For icon-only buttons, always provide an
aria-label
Variants
Menu Variants (menuVariant)
Chevron Variant (menuVariant="chevron")
The default variant shows text with a chevron indicator. Best for navigation menus or action lists.
Button Variant (menuVariant="button")
Styled as a button without the chevron. Combine with buttonVariant for color variants. Best for prominent actions.
Icon Variant (menuVariant="icon")
Displays only an icon (using unicode prop or children). Best for compact UIs or toolbar menus. Always requires an aria-label.
Button Variants (buttonVariant)
Since AgMenuButton wraps AgButton, you can use any button color variant:
primary- Primary action colorsecondary- Secondary action colorsuccess- Success/positive actionwarning- Warning/caution actiondanger- Destructive/dangerous actionmonochrome- Neutral monochrome style''(empty) - Default/unstyled
Menu Item Variants
Menu items support a variant prop for different visual styles:
default- Standard menu item with colored selection backgroundmonochrome- Monochrome style with neutral selection colors
Button Styling Options
All AgButton properties are available:
- size:
x-sm,sm,md,lg,xl - shape:
capsule,rounded,circle,square,rounded-square - bordered: Add border to button
- ghost: Transparent background with colored text/border
- link: Style as a link
Menu Alignment
The menuAlign prop controls the horizontal alignment of the menu relative to the trigger button:
left(default): Menu left edge aligns with button left edgeright: Menu right edge aligns with button right edge
This is particularly useful when the menu button is positioned near the right edge of the viewport, ensuring the menu stays within view.
<VueMenu menu-align="left">Options</VueMenu>
<VueMenu menu-align="right">Options</VueMenu>Placement Options
The menu popover can be positioned relative to the button:
bottom-start: Below button, aligned to start (default)bottom-end: Below button, aligned to endtop-start: Above button, aligned to starttop-end: Above button, aligned to end
Menu Types
The type prop on the <ag-menu> element controls the selection behavior:
type="default" (Navigation Menus)
Use for: Navigation, actions, commands (Profile, Logout, New File, etc.)
- Selection is transient - items show selected state only while hovering/focused
- Selection clears automatically when the menu closes
- No persistent checked state
- Follows WAI-ARIA
menuitempattern
<VueMenu menu-aria-label="User menu">
User
<template #menu>
<VueMenuItem value="profile">Profile</VueMenuItem>
<VueMenuItem value="settings">Settings</VueMenuItem>
<VueMenuItem value="logout">Logout</VueMenuItem>
</template>
</VueMenu>type="single-select" (Selection Menus)
Use for: Persistent selections (Sort by, Filter by, Choose theme, etc.)
- Selection is persistent - selected item shows checked state
- Selection remains after menu closes
- Use
selectedValueprop to control which item is checked - Follows WAI-ARIA
menuitemradiopattern
<VueMenu menu-aria-label="Sort options">
Sort by
<template #menu>
<ag-menu type="single-select" selected-value="date">
<VueMenuItem value="date">Date</VueMenuItem>
<VueMenuItem value="name">Name</VueMenuItem>
<VueMenuItem value="size">Size</VueMenuItem>
</ag-menu>
</template>
</VueMenu>Advanced Features
Additional Gutter
The additionalGutter prop allows you to add extra vertical spacing beyond the trigger button's height when positioning the menu. This is useful when the menu button is within a taller container (like a header) and you need the menu to clear that container.
React
import { ReactMenuButton, ReactMenu, ReactMenuItem } from "agnosticui-core/menu/react";
export default function HeaderMenu() {
return (
<header style={{ height: "60px", padding: "10px", background: "#f3f4f6" }}>
<ReactMenuButton
menuVariant="chevron"
additionalGutter="10px"
ariaLabel="Header menu"
>
Menu
<ReactMenu slot="menu" ariaLabel="Header menu options">
<ReactMenuItem value="option1">Option 1</ReactMenuItem>
<ReactMenuItem value="option2">Option 2</ReactMenuItem>
<ReactMenuItem value="option3">Option 3</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</header>
);
}Vue
<template>
<header style="height: 60px; padding: 10px; background: #f3f4f6;">
<VueMenu
menu-variant="chevron"
additional-gutter="10px"
aria-label="Header menu"
menu-aria-label="Header menu options"
>
Menu
<template #menu>
<VueMenuItem value="option1">Option 1</VueMenuItem>
<VueMenuItem value="option2">Option 2</VueMenuItem>
<VueMenuItem value="option3">Option 3</VueMenuItem>
</template>
</VueMenu>
</header>
</template>
<script>
import VueMenu, { VueMenuItem } from "agnosticui-core/menu/vue";
export default {
components: { VueMenu, VueMenuItem }
};
</script>Dynamic Icon Switching
The menu button exposes a data-menu-open attribute that changes based on the menu state. You can use this with CSS to dynamically switch icons when the menu opens/closes.
React
import { ReactMenuButton, ReactMenu, ReactMenuItem } from "agnosticui-core/menu/react";
import React from "react";
export default function DynamicIconMenu() {
return (
<>
<style>
{`
.dynamic-icon-menu .menu-icon,
.dynamic-icon-menu .close-icon {
transition: opacity #6366f1 ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] .close-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] .menu-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
`}
</style>
<ReactMenuButton
menuVariant="icon"
ghost
className="dynamic-icon-menu"
ariaLabel="Toggle menu"
>
<span className="menu-icon">☰</span>
<span className="close-icon">✕</span>
<ReactMenu slot="menu" ariaLabel="Navigation">
<ReactMenuItem value="home">Home</ReactMenuItem>
<ReactMenuItem value="about">About</ReactMenuItem>
<ReactMenuItem value="contact">Contact</ReactMenuItem>
</ReactMenu>
</ReactMenuButton>
</>
);
}Vue
<template>
<VueMenu
menu-variant="icon"
ghost
class="dynamic-icon-menu"
aria-label="Toggle menu"
menu-aria-label="Navigation"
>
<span class="menu-icon">☰</span>
<span class="close-icon">✕</span>
<template #menu>
<VueMenuItem value="home">Home</VueMenuItem>
<VueMenuItem value="about">About</VueMenuItem>
<VueMenuItem value="contact">Contact</VueMenuItem>
</template>
</VueMenu>
</template>
<script>
import VueMenu, { VueMenuItem } from "agnosticui-core/menu/vue";
export default {
components: { VueMenu, VueMenuItem }
};
</script>
<style scoped>
.dynamic-icon-menu :deep(.menu-icon),
.dynamic-icon-menu :deep(.close-icon) {
transition: opacity #6366f1 ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] :deep(.close-icon) {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] :deep(.menu-icon) {
opacity: 0;
pointer-events: none;
position: absolute;
}
</style>Lit
<style>
.dynamic-icon-menu .menu-icon,
.dynamic-icon-menu .close-icon {
transition: opacity #6366f1 ease-in-out;
}
.dynamic-icon-menu[data-menu-open="false"] .close-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
.dynamic-icon-menu[data-menu-open="true"] .menu-icon {
opacity: 0;
pointer-events: none;
position: absolute;
}
</style>
<ag-menu-button
menu-variant="icon"
ghost
class="dynamic-icon-menu"
aria-label="Toggle menu"
>
<span class="menu-icon">☰</span>
<span class="close-icon">✕</span>
<ag-menu slot="menu" aria-label="Navigation">
<ag-menu-item value="home">Home</ag-menu-item>
<ag-menu-item value="about">About</ag-menu-item>
<ag-menu-item value="contact">Contact</ag-menu-item>
</ag-menu>
</ag-menu-button>CSS Shadow Parts
| Part | Description |
|---|---|
ag-menu-trigger-chevron-button | The menu button (chevron variant) |
ag-menu-trigger-icon-button | The menu button (icon variant) |
ag-menu-trigger-regular-button | The menu button (button variant) |
ag-menu-label | The label inside the chevron button |
ag-menu-unicode-icon | The unicode icon in the icon button |
ag-menu-chevron-icon | The chevron icon in the chevron button |
ag-menu | The menu container |
ag-menu-item | A menu item |
ag-menu-item-button | A menu item button |
ag-menu-separator | A separator between menu items |
Customization Example
.custom-menu-button::part(ag-menu-trigger-chevron-button) {
background-color: #4a5568;
color: white;
border: 2px solid #2d3748;
border-radius: 8px;
}
.custom-menu-button::part(ag-menu-label) {
font-weight: bold;
}
.custom-menu-button::part(ag-menu-chevron-icon) {
color: #a0aec0;
}
.custom-menu::part(ag-menu) {
background-color: #2d3748;
border: 1px solid #4a5568;
border-radius: 8px;
}
.custom-menu::part(ag-menu-item) {
color: #e2e8f0;
}
.custom-menu::part(ag-menu-item):hover {
background-color: #4a5568;
}
.custom-menu::part(ag-menu-separator) {
background-color: #4a5568;
}