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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/menu';
@Component({ selector: 'app-menu-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <button type="button" id="card-actions" class="gui-button gui-button--outlined" popovertarget="card-actions-menu" > Actions </button> <gui-menu> <div id="card-actions-menu" popover> <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> </gui-menu> `,})export class MenuBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';</script>
<template> <button type="button" id="card-actions" class="gui-button gui-button--outlined" popovertarget="card-actions-menu" > Actions </button> <gui-menu> <div id="card-actions-menu" popover> <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> </gui-menu></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';
@customElement('menu-basic')export class MenuBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <button type="button" id="card-actions" class="gui-button gui-button--outlined" popovertarget="card-actions-menu" > Actions </button> <gui-menu> <div id="card-actions-menu" popover> <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> </gui-menu> `; }}<button type="button" id="card-actions" class="gui-button gui-button--outlined" popovertarget="card-actions-menu"> Actions</button><gui-menu> <div id="card-actions-menu" popover> <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></gui-menu>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.
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> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/menu';
@Component({ selector: 'app-menu-example', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-menu
> <div id="card-actions-menu" popover="auto"> <button type="button" value="edit">Edit</button> <button type="button" value="delete">Delete</button> </div> </gui-menu> `,})export class MenuExampleComponent {}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';</script>
<template> <gui-menu
> <div id="card-actions-menu" popover="auto"> <button type="button" value="edit">Edit</button> <button type="button" value="delete">Delete</button> </div> </gui-menu></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';
@customElement('menu-example')export class MenuExample extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-menu
> <div id="card-actions-menu" popover="auto"> <button type="button" value="edit">Edit</button> <button type="button" value="delete">Delete</button> </div> </gui-menu> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-menu > <div id="card-actions-menu" popover="auto"> <button type="button" value="edit">Edit</button> <button type="button" value="delete">Delete</button> </div></gui-menu>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';<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. |
Examples
Section titled “Examples”Picking an action
Section titled “Picking an action”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMenuEvents } from '@golemui/gui-components';import '@golemui/gui-components/menu';
type MenuEvent<K extends keyof typeof GuiMenuEvents> = GuiEventMap< typeof GuiMenuEvents>[K];
@Component({ selector: 'app-menu-events', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <button type="button" id="file-actions" class="gui-button gui-button--outlined" popovertarget="file-actions-menu" > File </button> <gui-menu id="file-menu" (gui-action)="onAction($event)"> <div id="file-actions-menu" popover> <button type="button" value="rename">Rename</button> <button type="button" value="duplicate">Duplicate</button> <hr /> <button type="button" value="delete">Delete</button> </div> </gui-menu> <p id="file-status" role="status"> {{ action() ? 'You picked ' + action() + '.' : 'No action yet.' }} </p> `,})export class MenuEventsComponent { action = signal('');
onAction(event: Event) { this.action.set(String((event as MenuEvent<'gui-action'>).detail.value)); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';
const action = ref('');</script>
<template> <button type="button" id="file-actions" class="gui-button gui-button--outlined" popovertarget="file-actions-menu" > File </button> <gui-menu id="file-menu" @gui-action="action = String($event.detail.value)"> <div id="file-actions-menu" popover> <button type="button" value="rename">Rename</button> <button type="button" value="duplicate">Duplicate</button> <hr /> <button type="button" value="delete">Delete</button> </div> </gui-menu> <p id="file-status" role="status"> {{ action ? `You picked ${action}.` : 'No action yet.' }} </p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiMenuEvents } from '@golemui/gui-components';import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';
type MenuEvent<K extends keyof typeof GuiMenuEvents> = GuiEventMap< typeof GuiMenuEvents>[K];
@customElement('menu-events')export class MenuEvents extends LitElement { @state() private action = '';
protected override createRenderRoot() { return this; }
private onAction(event: MenuEvent<'gui-action'>) { this.action = String(event.detail.value); }
override render() { return html` <button type="button" id="file-actions" class="gui-button gui-button--outlined" popovertarget="file-actions-menu" > File </button> <gui-menu id="file-menu" @gui-action=${this.onAction}> <div id="file-actions-menu" popover> <button type="button" value="rename">Rename</button> <button type="button" value="duplicate">Duplicate</button> <hr /> <button type="button" value="delete">Delete</button> </div> </gui-menu> <p id="file-status" role="status"> ${this.action ? `You picked ${this.action}.` : 'No action yet.'} </p> `; }}<button type="button" id="file-actions" class="gui-button gui-button--outlined" popovertarget="file-actions-menu"> File</button><gui-menu id="file-menu"> <div id="file-actions-menu" popover> <button type="button" value="rename">Rename</button> <button type="button" value="duplicate">Duplicate</button> <hr /> <button type="button" value="delete">Delete</button> </div></gui-menu><p id="file-status" role="status">No action yet.</p>
<script type="module"> const menu = document.querySelector('#file-menu');
menu.addEventListener('gui-action', (event) => { document.querySelector('#file-status').textContent = `You picked ${event.detail.value}.`; });</script>Links and disabled items
Section titled “Links and disabled items”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/menu';
@Component({ selector: 'app-menu-links', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <button type="button" id="help-button" class="gui-button gui-button--outlined" popovertarget="help-menu" > Help </button> <gui-menu placement="bottom-end"> <div id="help-menu" popover> <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> </gui-menu> `,})export class MenuLinksComponent {}<script setup lang="ts">import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';</script>
<template> <button type="button" id="help-button" class="gui-button gui-button--outlined" popovertarget="help-menu" > Help </button> <gui-menu placement="bottom-end"> <div id="help-menu" popover> <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> </gui-menu></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/menu';import '@golemui/gui-components/index.css';
@customElement('menu-links')export class MenuLinks extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <button type="button" id="help-button" class="gui-button gui-button--outlined" popovertarget="help-menu" > Help </button> <gui-menu placement="bottom-end"> <div id="help-menu" popover> <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> </gui-menu> `; }}<button type="button" id="help-button" class="gui-button gui-button--outlined" popovertarget="help-menu"> Help</button><gui-menu placement="bottom-end"> <div id="help-menu" popover> <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></gui-menu>Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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:
<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:
/* Destructive actions in the error color. */gui-menu > [popover] > [value='delete'] { color: var(--gui-intent-error);}Tokens
Section titled “Tokens”The menu reads these design tokens. Set them on :root to change every menu, or on any element
to change the menus inside it:
| Token | What it styles |
|---|---|
--gui-bg-default | The background of the panel. |
--gui-border-default | The border of the panel, and the separators. |
--gui-radius-md | The corner radius of the panel. |
--gui-shadow-md | The shadow of the panel. |
--gui-text-default | The text of the items. |
--gui-bg-surface-hover | An item under the pointer or with keyboard focus. |
--gui-border-focus | The focus outline of an item. |
--gui-text-disabled | A disabled item. |
--gui-radius-sm | The corner radius of an item. |
--gui-space-1 | The padding of the panel, and the space around a separator. |
--gui-space-2 | The padding above and below an item. |
--gui-space-3 | The padding before and after an item’s text. |
--gui-overlay-duration | How 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.
Accessibility
Section titled “Accessibility”The menu follows the menu button pattern of the ARIA Authoring Practices.
- Roles. The panel has the
menurole and its items themenuitemrole. The button hasaria-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 anaria-labelto 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.
| Key | Action |
|---|---|
| Enter Space | On 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 End | The first or the last item. |
| A letter | The next item whose text starts with the letters typed. |
| Enter Space | On an item: picks it, closes the menu and returns the focus to the button. |
| Escape | Closes the menu and returns the focus to the button. |
| Tab | Closes the menu. |
What every element shares is in Accessibility.