Menu
A strict action menu with managed focus, keyboard navigation, and menu button semantics.
Usage
Menu is a strict action menu primitive. Use Menu, MenuTrigger, MenuContent, and MenuItem together.
<div class="zui-menu" data-state="open">
<button class="zui-button zui-button-variant-outline zui-menu-trigger" aria-haspopup="menu" aria-expanded="true">
Open menu <i class="ph ph-caret-down"></i>
</button>
<div class="zui-menu-content" role="menu" data-state="open">
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Profile</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Settings</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Logout</button>
</div>
</div>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/react'
<Menu>
<MenuTrigger>Open menu <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
<MenuItem>Logout</MenuItem>
</MenuContent>
</Menu>---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu>
<MenuTrigger>Open menu <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
<MenuItem>Logout</MenuItem>
</MenuContent>
</Menu>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/solid'
<Menu>
<MenuTrigger>Open menu <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
<MenuItem>Logout</MenuItem>
</MenuContent>
</Menu><script lang="ts">
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/svelte'
</script>
<Menu>
<MenuTrigger>Open menu <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
<MenuItem>Logout</MenuItem>
</MenuContent>
</Menu><template>
<Menu>
<MenuTrigger>Open menu <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
<MenuItem>Logout</MenuItem>
</MenuContent>
</Menu>
</template>
<script setup>
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/vue'
</script>With icons and links
<div class="zui-menu" data-state="open">
<button class="zui-button zui-menu-trigger" aria-haspopup="menu" aria-expanded="true">Account <i class="ph ph-caret-down"></i></button>
<div class="zui-menu-content" role="menu" data-state="open">
<a href="#" class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem"><i class="ph ph-user"></i> Profile</a>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem"><i class="ph ph-gear"></i> Settings</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem"><i class="ph ph-sign-out"></i> Logout</button>
</div>
</div>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/react'
<Menu>
<MenuTrigger>Account <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem href="#"><i className="ph ph-user" /> Profile</MenuItem>
<MenuItem><i className="ph ph-gear" /> Settings</MenuItem>
<MenuItem><i className="ph ph-sign-out" /> Logout</MenuItem>
</MenuContent>
</Menu>---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu>
<MenuTrigger>Account <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem href="#"><i class="ph ph-user" /> Profile</MenuItem>
<MenuItem><i class="ph ph-gear" /> Settings</MenuItem>
<MenuItem><i class="ph ph-sign-out" /> Logout</MenuItem>
</MenuContent>
</Menu>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/solid'
<Menu>
<MenuTrigger>Account <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem href="#"><i class="ph ph-user" /> Profile</MenuItem>
<MenuItem><i class="ph ph-gear" /> Settings</MenuItem>
<MenuItem><i class="ph ph-sign-out" /> Logout</MenuItem>
</MenuContent>
</Menu><script lang="ts">
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/svelte'
</script>
<Menu>
<MenuTrigger>Account <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem href="#"><i class="ph ph-user"></i> Profile</MenuItem>
<MenuItem><i class="ph ph-gear"></i> Settings</MenuItem>
<MenuItem><i class="ph ph-sign-out"></i> Logout</MenuItem>
</MenuContent>
</Menu><template>
<Menu>
<MenuTrigger>Account <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem href="#"><i class="ph ph-user" /> Profile</MenuItem>
<MenuItem><i class="ph ph-gear" /> Settings</MenuItem>
<MenuItem><i class="ph ph-sign-out" /> Logout</MenuItem>
</MenuContent>
</Menu>
</template>
<script setup>
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/vue'
</script>Shape
Use the shape prop on Menu to change the corners of the menu panel: default (rounded), hard (square) or squircle. Menu has no soft shape, because a pill shape does not suit a large panel. A squircle is a rounded square with smooth corners. Browsers without corner-shape support show normal rounded corners. Open each menu to compare.
<div class="zui-menu">
<button class="zui-button zui-button-variant-outline zui-menu-trigger" aria-haspopup="menu" aria-expanded="false">
Default <i class="ph ph-caret-down"></i>
</button>
<div class="zui-menu-content" role="menu" popover="manual">
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Profile</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Settings</button>
</div>
</div>
<div class="zui-menu zui-menu-shape-hard">
<button class="zui-button zui-button-variant-outline zui-menu-trigger" aria-haspopup="menu" aria-expanded="false">
Hard <i class="ph ph-caret-down"></i>
</button>
<div class="zui-menu-content" role="menu" popover="manual">
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Profile</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Settings</button>
</div>
</div>
<div class="zui-menu zui-menu-shape-squircle">
<button class="zui-button zui-button-variant-outline zui-menu-trigger" aria-haspopup="menu" aria-expanded="false">
Squircle <i class="ph ph-caret-down"></i>
</button>
<div class="zui-menu-content" role="menu" popover="manual">
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Profile</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Settings</button>
</div>
</div>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/react'
<Menu>
<MenuTrigger>Default <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="hard">
<MenuTrigger>Hard <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="squircle">
<MenuTrigger>Squircle <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu>
<MenuTrigger>Default <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="hard">
<MenuTrigger>Hard <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="squircle">
<MenuTrigger>Squircle <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/solid'
<Menu>
<MenuTrigger>Default <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="hard">
<MenuTrigger>Hard <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="squircle">
<MenuTrigger>Squircle <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu><script lang="ts">
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/svelte'
</script>
<Menu>
<MenuTrigger>Default <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="hard">
<MenuTrigger>Hard <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="squircle">
<MenuTrigger>Squircle <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu><template>
<Menu>
<MenuTrigger>Default <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="hard">
<MenuTrigger>Hard <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
<Menu shape="squircle">
<MenuTrigger>Squircle <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Profile</MenuItem>
<MenuItem>Settings</MenuItem>
</MenuContent>
</Menu>
</template>
<script setup>
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/vue'
</script>To make squircle the default everywhere, add zui-squircle-all to <html> — see Squircle.
Anatomy
Menu— root state containerMenuTrigger— button that opens and closes the menuMenuContent— popup surface withrole="menu"MenuItem— actionable row rendered as abuttonora
Common props
Menu
defaultOpen— uncontrolled initial open stateopen— controlled open statedisabled— disables the whole menuside—top | right | bottom | leftalign—start | center | enddir—ltr | rtl
MenuTrigger
MenuTrigger accepts the same visual props as a ZUI button:
variantcolorsizeshapeicon
MenuItem
href— render as a link itemdisabled— prevent focus/activationtextValue— explicit label used for typeahead matching
Positioning
Use side and align on Menu to change where the menu surface appears.
The menu uses CSS anchor positioning to stay next to its trigger. If the menu does not fit in the viewport, it moves to the other side of the trigger. For example, a menu near the right edge of the screen opens to the left.
---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu side="top" align="end">
<MenuTrigger variant="outline">More <i class="ph ph-dots-three" /></MenuTrigger>
<MenuContent>
<MenuItem>Rename</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Archive</MenuItem>
</MenuContent>
</Menu>Typeahead
When the menu is open, typing letters moves focus to the next matching enabled item.
By default, matching uses each item's text content. Pass textValue when the visible label includes icons, extra formatting, or text that should not drive matching.
---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu>
<MenuTrigger variant="outline">Jump to item</MenuTrigger>
<MenuContent>
<MenuItem textValue="profile"><i class="ph ph-user" /> Profile</MenuItem>
<MenuItem textValue="settings"><i class="ph ph-gear" /> Settings</MenuItem>
<MenuItem textValue="sign out"><i class="ph ph-sign-out" /> Sign out</MenuItem>
</MenuContent>
</Menu>Keyboard behavior
Enter,Space,ArrowDownon the trigger open the menu and focus the first enabled item.ArrowUpon the trigger opens the menu and focuses the last enabled item.- Inside the menu, use
ArrowDown,ArrowUp,Home, andEndto move focus. EnterandSpaceactivate the focused item.Escapecloses the menu and returns focus to the trigger.Tabcloses the menu and continues normal tab order.- Typeahead is supported for enabled items.
Dismissal behavior
The menu closes on outside click, trigger toggle, Escape, Tab, and item activation.
Controlled and uncontrolled state
Use defaultOpen for uncontrolled menus. Use open when the open state comes from your app.
In Astro docs examples, open is best used as a static initial state example. Interactive controlled behavior is most useful in framework wrappers like React, Solid, Svelte, and Vue.
Disabled items
Pass disabled to Menu to disable the whole primitive, or to individual MenuItems to keep them in the list but prevent activation.
<div class="zui-menu" data-state="open">
<button class="zui-button zui-button-variant-outline zui-menu-trigger" aria-haspopup="menu" aria-expanded="true">Actions <i class="ph ph-caret-down"></i></button>
<div class="zui-menu-content" role="menu" data-state="open">
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Edit</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem" disabled>Archive</button>
<button class="zui-menu-item zui-button zui-button-variant-ghost" role="menuitem">Delete</button>
</div>
</div>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/react'
<Menu>
<MenuTrigger variant="outline">Actions <i className="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Edit</MenuItem>
<MenuItem disabled>Archive</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuContent>
</Menu>---
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/astro'
---
<Menu>
<MenuTrigger variant="outline">Actions <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Edit</MenuItem>
<MenuItem disabled>Archive</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuContent>
</Menu>import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/solid'
<Menu>
<MenuTrigger variant="outline">Actions <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Edit</MenuItem>
<MenuItem disabled>Archive</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuContent>
</Menu><script lang="ts">
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/svelte'
</script>
<Menu>
<MenuTrigger variant="outline">Actions <i class="ph ph-caret-down"></i></MenuTrigger>
<MenuContent>
<MenuItem>Edit</MenuItem>
<MenuItem disabled>Archive</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuContent>
</Menu><template>
<Menu>
<MenuTrigger variant="outline">Actions <i class="ph ph-caret-down" /></MenuTrigger>
<MenuContent>
<MenuItem>Edit</MenuItem>
<MenuItem disabled>Archive</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuContent>
</Menu>
</template>
<script setup>
import { Menu, MenuTrigger, MenuContent, MenuItem } from '@mrmartineau/zui/vue'
</script>Notes
- Menu is a strict action-menu primitive, not a generic popover panel.
MenuTriggeris always button-based.MenuItemrenders as a button by default, or as a link whenhrefis provided.- Disabled items are skipped by keyboard navigation and typeahead.
- Menu content stays mounted. It is a manual popover (
popover="manual"), so it opens in the top layer above all other content.
Browser support
The menu uses the Popover API and CSS anchor positioning. The Popover API works in all current browsers. Anchor positioning needs Chrome or Edge 125, Safari 26 or Firefox 147 or later.
In older browsers, the menu shows in the centre of the viewport, not next to its trigger. To get the correct position in these browsers, load the CSS anchor positioning polyfill in your app.
CSS custom properties
Set these on :root, on a wrapper, or on the element. See Component custom properties.
| Property | Default | Description |
|---|---|---|
--zui-menu-radius |
var(--radius-xl) |
Panel border radius |
--zui-menu-border |
var(--color-border) |
Panel border colour |
--zui-menu-bg |
var(--color-surface) |
Panel background |
--zui-menu-padding |
var(--space-2xs) |
Panel padding |
--zui-menu-shadow |
var(--shadow-md) |
Panel box shadow |
--zui-menu-min-width |
12.5rem |
Panel minimum width |
--zui-menu-item-highlight-bg |
oklch(from var(--color-text) l c h / 0.08) |
Background of the highlighted item |