Repo

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>
<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>

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>

To make squircle the default everywhere, add zui-squircle-all to <html> — see Squircle.

Anatomy

Common props

MenuTrigger accepts the same visual props as a ZUI button:

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

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>

Notes

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

Theme

Copy this CSS to your project: