Skip to content
Components, switch product

Menu

<gui-menu> shows a list of actions under the button that opens it, such as the actions of a card or a row. You write a popover panel of buttons and links; the element makes it a menu: the roles, the keyboard of the menu button pattern, and an event when the user picks an item.


Code of the example above
MenuBasic.tsx
import { GuiMenu } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MenuBasic() {
return (
<>
<button
type="button"
id="card-actions"
className="gui-button gui-button--outlined"
popoverTarget="card-actions-menu"
>
Actions
</button>
<GuiMenu>
<div id="card-actions-menu" popover="auto">
<button type="button" value="edit">
Edit
</button>
<button type="button" value="duplicate">
Duplicate
</button>
<button type="button" value="move">
Move to…
</button>
<hr />
<button type="button" value="delete">
Delete
</button>
</div>
</GuiMenu>
</>
);
}

The element’s entry point is @golemui/gui-components/menu, which registers <gui-menu> and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

MenuExample.tsx
import { GuiMenu } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MenuExample() {
return (
<GuiMenu
>
<div id="card-actions-menu" popover="auto">
<button type="button" value="edit">Edit</button>
<button type="button" value="delete">Delete</button>
</div>
</GuiMenu>
);
}

<gui-menu> is a gui-popover: the panel is its child with the popover attribute, a native <button> whose popovertarget names the panel opens it, and placement, anchor, open and gui-toggle work the same way. Give the button an id: the menu takes its name from the button.

The items are the panel’s own children: its buttons, its links with an href, and any element you give the menuitem role. An <hr> between them is a separator. Items inside a wrapper element aren’t part of the menu.

Properties

Property Type Default Description
items read-only, no attribute HTMLElement[] – The menu's items, in order.
placement GuiPlacement 'bottom-start' Where the panel sits next to its anchor. Start and end follow the reading direction.
anchor string – The id of the element to anchor the panel to, when it is not the panel's invoker.
open boolean – Whether the panel is open. Without a value it takes the panel's own state. Setting it opens or closes the panel and fires no gui-toggle.
panel read-only, no attribute HTMLElement | null – The app's panel: the element's [popover] child.

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-toggle The menu opened or closed. detail.open is the new state.
gui-action The user picked an item. detail.value is its value (or its text), and detail.item the item.

When the user picks an item, with a click, Enter or Space, the element fires gui-action, closes the menu and returns the focus to the button. event.detail.value is the item’s value, or its text when it has none, and event.detail.item is the item itself.


No action yet.

Code of the example above
MenuEvents.tsx
import { useState } from 'react';
import { GuiMenu } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MenuEvents() {
const [action, setAction] = useState('');
return (
<>
<button
type="button"
id="file-actions"
className="gui-button gui-button--outlined"
popoverTarget="file-actions-menu"
>
File
</button>
<GuiMenu
id="file-menu"
onGuiAction={(event) => setAction(String(event.detail.value))}
>
<div id="file-actions-menu" popover="auto">
<button type="button" value="rename">
Rename
</button>
<button type="button" value="duplicate">
Duplicate
</button>
<hr />
<button type="button" value="delete">
Delete
</button>
</div>
</GuiMenu>
<p id="file-status" role="status">
{action ? `You picked ${action}.` : 'No action yet.'}
</p>
</>
);
}

A link in the menu navigates like any link, and fires gui-action first. A disabled button, or an item with aria-disabled="true", stays in the menu but can’t be picked, and the arrow keys skip it. placement sets where the menu opens, as on the popover: bottom-end lines it up with the end of the button.

Documentation Accessibility
Code of the example above
MenuLinks.tsx
import { GuiMenu } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function MenuLinks() {
return (
<>
<button
type="button"
id="help-button"
className="gui-button gui-button--outlined"
popoverTarget="help-menu"
>
Help
</button>
<GuiMenu placement="bottom-end">
<div id="help-menu" popover="auto">
<a href="/components/docs/getting-started/introduction/">
Documentation
</a>
<a href="/components/docs/guides/accessibility/">Accessibility</a>
<hr />
<button type="button" disabled>
Live chat (offline)
</button>
</div>
</GuiMenu>
</>
);
}

<gui-menu> has no box of its own (display: contents). It sets the roles and the ARIA attributes on your markup, unless you set them yourself:

gui-menu
<button
type="button"
id="card-actions"
popovertarget="card-actions-menu"
aria-haspopup="menu"
>
Actions
</button>
<gui-menu open>
<div
id="card-actions-menu"
popover
role="menu"
aria-labelledby="card-actions"
>
<button type="button" value="edit" role="menuitem" tabindex="-1">
Edit
</button>
<button type="button" value="duplicate" role="menuitem" tabindex="-1">
Duplicate
</button>
<hr />
<button type="button" value="delete" role="menuitem" tabindex="-1">
Delete
</button>
</div>
</gui-menu>

The stylesheet draws the panel as a card at least 10rem wide, and each item as a row as wide as the menu. While the menu is open, the element places the panel with inline styles, as the popover does: see Floating panels.

Select the panel as gui-menu > [popover] and the items as its children:

styles.css
/* Destructive actions in the error color. */
gui-menu > [popover] > [value='delete'] {
color: var(--gui-intent-error);
}

The menu reads these design tokens. Set them on :root to change every menu, or on any element to change the menus inside it:

TokenWhat it styles
--gui-bg-defaultThe background of the panel.
--gui-border-defaultThe border of the panel, and the separators.
--gui-radius-mdThe corner radius of the panel.
--gui-shadow-mdThe shadow of the panel.
--gui-text-defaultThe text of the items.
--gui-bg-surface-hoverAn item under the pointer or with keyboard focus.
--gui-border-focusThe focus outline of an item.
--gui-text-disabledA disabled item.
--gui-radius-smThe corner radius of an item.
--gui-space-1The padding of the panel, and the space around a separator.
--gui-space-2The padding above and below an item.
--gui-space-3The padding before and after an item’s text.
--gui-overlay-durationHow long the menu takes to fade in and out. Reduced motion skips it.

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

The menu follows the menu button pattern of the ARIA Authoring Practices.

  • Roles. The panel has the menu role and its items the menuitem role. The button has aria-haspopup="menu", and the browser reports whether the menu is expanded.
  • Name. The menu is named after its button, through aria-labelledby. When the button has no id, the menu has no name, and the element logs a warning in development. Give the panel an aria-label to name it yourself.
  • Focus. Opening the menu moves the focus to its first item. The items are out of the tab order: the arrow keys move between them, and Tab leaves the menu.
  • Your roles win. A role or name you set on the panel, an item or the button stays as you wrote it.
KeyAction
Enter SpaceOn the button: opens the menu, on the first item.
↓On the button: opens the menu, on the first item. In the menu: the next item, from the last back to the first.
↑On the button: opens the menu, on the last item. In the menu: the previous item, from the first to the last.
Home EndThe first or the last item.
A letterThe next item whose text starts with the letters typed.
Enter SpaceOn an item: picks it, closes the menu and returns the focus to the button.
EscapeCloses the menu and returns the focus to the button.
TabCloses the menu.

What every element shares is in Accessibility.