Menu
A list of actions in a popover, placed against its button.
On this page
A menu is a popover that holds a list of actions. Add the popover attribute and .menu to a container, fill it with menu-item buttons and links, and point a button at it with popovertarget. The browser opens and closes it; CSS anchor positioning places it against the button and flips it at the viewport edge.
Menu
<button type="button" class="btn btn-square" popovertarget="project-menu" aria-label="Project actions"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <circle cx="5" cy="12" r="1" /> <circle cx="12" cy="12" r="1" /> <circle cx="19" cy="12" r="1" /> </svg></button><div id="project-menu" popover class="menu"> <button type="button" class="menu-item" popovertarget="project-menu" popovertargetaction="hide"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M4 20h4L19 9l-4-4L4 16v4Z" /> </svg>Rename <span class="menu-shortcut">⌘R</span> </button> <button type="button" class="menu-item" popovertarget="project-menu" popovertargetaction="hide"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <rect x="8" y="8" width="12" height="12" rx="2" /> <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /> </svg>Duplicate <span class="menu-shortcut">⌘D</span> </button> <a class="menu-item" href="#settings"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M4 6h10M18 6h2M4 12h2M10 12h10M4 18h10M18 18h2" /> <circle cx="16" cy="6" r="2" /> <circle cx="8" cy="12" r="2" /> <circle cx="16" cy="18" r="2" /> </svg>Settings</a> <hr class="menu-separator"> <button type="button" class="menu-item menu-item-danger" popovertarget="project-menu" popovertargetaction="hide"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3" /> </svg>Delete</button></div>#Items
A menu-item is a <button> for an action or an <a> for navigation. The row under the pointer takes a chip tone at once, without a fade, so the menu keeps up with a moving pointer; a row reached by the keys also takes an ink outline. Put icon on an icon: the item sets its size and keeps it quiet until the row is in use.
- A link closes the menu by navigating. A button closes it with
popovertargetandpopovertargetaction="hide", as above; leave them out to keep the menu open. menu-item-dangergives a destructive action the danger ink.menu-shortcutsets a shortcut at the end of the row.- A disabled button drops to the disabled ink. For a link, use
aria-disabled="true"and remove thehref.
#Groups
menu-label captions a group and menu-separator on an <hr> draws a hairline between groups, running to the menu's edges.
Groups
<button type="button" class="btn" popovertarget="account-menu">Ada Lovelace</button><div id="account-menu" popover class="menu"> <div class="menu-label">Account</div> <a class="menu-item" href="#profile">Profile</a> <a class="menu-item" href="#billing">Billing</a> <hr class="menu-separator"> <div class="menu-label">Team</div> <a class="menu-item" href="#members">Members</a> <button type="button" class="menu-item" disabled>Transfer ownership</button> <hr class="menu-separator"> <a class="menu-item" href="#sign-out">Sign out</a></div>#Checkable items
For options that stay on, make the item a <label> around a native checkbox or radio with menu-input. The input covers the row invisibly and keeps the state, the keys and the form value; the row shows a check at its end and reserves that space, so labels do not move.
Checkable items
<button type="button" class="btn" popovertarget="view-menu">View</button><div id="view-menu" popover class="menu"> <div class="menu-label">Show</div> <label class="menu-item"> <input type="checkbox" class="menu-input" checked>Archived projects</label> <label class="menu-item"> <input type="checkbox" class="menu-input">Preview deployments</label> <hr class="menu-separator"> <div class="menu-label">Sort by</div> <label class="menu-item"> <input type="radio" class="menu-input" name="sort" checked>Last updated</label> <label class="menu-item"> <input type="radio" class="menu-input" name="sort">Name</label></div>Items driven by script can carry aria-checked="true" instead, with role="menuitemcheckbox" or role="menuitemradio".
#Placement
A menu opens below its button, lined up with the start edge. menu-end lines it up with the end edge, for a button at the end of a row or a toolbar; menu-top opens it above. It flips on its own when there is no room.
Placement
<button type="button" class="btn btn-quiet btn-square" popovertarget="row-menu" aria-label="Row actions"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <circle cx="5" cy="12" r="1" /> <circle cx="12" cy="12" r="1" /> <circle cx="19" cy="12" r="1" /> </svg></button><div id="row-menu" popover class="menu menu-end"> <button type="button" class="menu-item" popovertarget="row-menu" popovertargetaction="hide">Open</button> <button type="button" class="menu-item" popovertarget="row-menu" popovertargetaction="hide">Copy link</button> <button type="button" class="menu-item" popovertarget="row-menu" popovertargetaction="hide">Archive</button></div>#Submenu
An item can open another menu. Give the item popovertarget and aria-haspopup="menu", and put a menu menu-sub right after it, inside the parent menu. The item gains a chevron and stays lit while its submenu is open. The submenu opens beside the item, lined up with it, and flips to the other side when there is no room. Popovers nest, so the parent stays open, and Esc closes one level at a time.
Submenu
<button type="button" class="btn" popovertarget="file-menu">File</button><div id="file-menu" popover class="menu"> <button type="button" class="menu-item">New project <span class="menu-shortcut">⌘N</span> </button> <button type="button" class="menu-item" popovertarget="recent-menu" aria-haspopup="menu">Open recent</button> <div id="recent-menu" popover class="menu menu-sub"> <button type="button" class="menu-item">atlas</button> <button type="button" class="menu-item">borealis</button> <button type="button" class="menu-item">cirrus</button> </div> <hr class="menu-separator"> <button type="button" class="menu-item">Export <span class="menu-shortcut">⌘E</span> </button></div>A submenu opens on a click, which works the same for a pointer, a finger and the keys. Opening it on hover as well is a script's choice.
#The behavior
Without script a menu is complete: the browser opens and closes it, Tab moves through the items and Esc returns focus to the button. The menu behavior is optional. It adds the keys of a menu, for an application whose readers expect the arrows.
With the behavior
<button type="button" class="btn" popovertarget="edit-menu" aria-haspopup="menu">Edit</button><div id="edit-menu" popover class="menu" role="menu" aria-label="Edit"> <button type="button" class="menu-item" role="menuitem" popovertarget="edit-menu" popovertargetaction="hide">Undo <span class="menu-shortcut">⌘Z</span> </button> <button type="button" class="menu-item" role="menuitem" popovertarget="edit-menu" popovertargetaction="hide">Redo <span class="menu-shortcut">⇧⌘Z</span> </button> <button type="button" class="menu-item" role="menuitem" disabled>Paste</button> <hr class="menu-separator"> <button type="button" class="menu-item" role="menuitem" popovertarget="share-menu" aria-haspopup="menu">Share</button> <div id="share-menu" popover class="menu menu-sub" role="menu" aria-label="Share"> <button type="button" class="menu-item" role="menuitem" popovertarget="edit-menu" popovertargetaction="hide">Copy link</button> <button type="button" class="menu-item" role="menuitem" popovertarget="edit-menu" popovertargetaction="hide">Send by email</button> </div> <hr class="menu-separator"> <button type="button" class="menu-item" role="menuitem" popovertarget="edit-menu" popovertargetaction="hide">Select all <span class="menu-shortcut">⌘A</span> </button></div><script type="module"> import { attach } from '@master/ui/behaviors' attach()</script>menu takes the element with the menu class and the popover attribute, has no options, and returns the function that detaches it. A submenu is a menu: attach the behavior to it too, as attach() does.
import { menu } from '@master/ui/behaviors/menu.js'for (const element of document.querySelectorAll('.menu')) menu(element)- Opening. When the menu opens, focus moves to its first item.
- Moving. The arrow keys, Home, End and typed letters move focus through the items, skipping hidden and disabled ones. Tab closes the menu.
- Submenus. On an item with
aria-haspopup="menu"that amenu-subfollows, the arrow that points into the submenu opens it, and the opposite arrow closes it and returns to the item. The two change places in a right-to-left menu. - Items stay native. A button, a link, and the checkbox or radio of a label item run with Enter and Space as they always do. The behavior sets no state and dispatches no event: listen for
clickon an item andchangeon amenu-input. - Roles. The behavior adds none. Write
role="menu"on the menu androle="menuitem"on its items together with it, as the preview does.
#Context menu
menu-context opens a menu at the pointer instead of against a button. The platform has no declarative way to open a popover from a right click, so the menu behavior exports contextMenu(target, menu) for it. On contextmenu, which a right click and a long press send, it prevents the browser's own menu, writes the pointer's position into --menu-x and --menu-y and calls showPopover(). The stylesheet does the rest. The menu opens down and to the end of that point, and @position-try turns it toward the middle when it would cross an edge of the viewport, with no measuring.
Context menu
<div class="well flex items-center justify-center h(200) text-sm fg-text-muted" id="canvas" tabindex="0">Right-click or long-press here, or focus it and press Shift F10</div><div id="canvas-menu" popover class="menu menu-context min-w(160)" role="menu" aria-label="Canvas"> <button type="button" class="menu-item" role="menuitem" popovertarget="canvas-menu" popovertargetaction="hide">Copy <span class="menu-shortcut">⌘C</span> </button> <button type="button" class="menu-item" role="menuitem" popovertarget="canvas-menu" popovertargetaction="hide">Paste <span class="menu-shortcut">⌘V</span> </button> <button type="button" class="menu-item" role="menuitem" popovertarget="canvas-menu" popovertargetaction="hide">Duplicate</button> <hr class="menu-separator"> <button type="button" class="menu-item menu-item-danger" role="menuitem" popovertarget="canvas-menu" popovertargetaction="hide">Delete</button></div><script> addEventListener('DOMContentLoaded', function () { MasterUI.contextMenu(document.getElementById('canvas'), document.getElementById('canvas-menu')) })</script><script type="module"> import { attach } from '@master/ui/behaviors' attach()</script>A context menu belongs to a target that only the page knows, so attach() does not attach one. The preview calls contextMenu itself; MasterUI is the bundle a preview loads. In a project, import both functions: contextMenu opens the menu, and menu gives it the keys inside, which attach() did in the preview.
import { contextMenu, menu } from '@master/ui/behaviors/menu.js'const target = document.getElementById('canvas')const list = document.getElementById('canvas-menu')contextMenu(target, list)menu(list)The ContextMenu key and Shift F10 open the menu at the middle of the target. The target must be focusable to hear them, so it has tabindex="0". Not every browser sends contextmenu for these keys, so the behavior listens for the keys as well. contextMenu returns the function that detaches it; calling it again for the same target replaces the first call.
#Menubar
A row of menus is a toolbar of quiet buttons, each with its own menu. A quiet button stays sunk into a well while the menu or popover right after it is open, so the open menu's title is marked without script.
Menubar
<div class="toolbar" role="toolbar" aria-label="Application"> <button type="button" class="btn btn-quiet btn-sm" popovertarget="bar-file">File</button> <div id="bar-file" popover class="menu"> <button type="button" class="menu-item">New <span class="menu-shortcut">⌘N</span> </button> <button type="button" class="menu-item">Open <span class="menu-shortcut">⌘O</span> </button> <button type="button" class="menu-item">Save <span class="menu-shortcut">⌘S</span> </button> </div> <button type="button" class="btn btn-quiet btn-sm" popovertarget="bar-edit">Edit</button> <div id="bar-edit" popover class="menu"> <button type="button" class="menu-item">Undo <span class="menu-shortcut">⌘Z</span> </button> <button type="button" class="menu-item">Redo <span class="menu-shortcut">⇧⌘Z</span> </button> </div> <button type="button" class="btn btn-quiet btn-sm" popovertarget="bar-view">View</button> <div id="bar-view" popover class="menu"> <label class="menu-item"> <input type="checkbox" class="menu-input" checked>Sidebar</label> <label class="menu-item"> <input type="checkbox" class="menu-input">Minimap</label> </div></div>#Keyboard
| Keys | Action |
|---|---|
| Enter, Space | On the button with popovertarget: open or close the menu. On an item: run it, or open its submenu |
| Tab, Shift+Tab | Without the behavior: move through the items. With the behavior: close the menu and move on from its button |
| Escape | Close the menu, one level at a time when a submenu is open, and return focus to the button |
| ArrowDown, ArrowUp | With the behavior: move to the next or the previous item, skipping disabled ones and wrapping at the ends |
| Home, End | With the behavior: move to the first or the last item |
| ArrowRight, ArrowLeft | With the behavior: open the submenu of the focused item and move into it, or close the submenu and return to its item; the two change places in a right-to-left menu |
| Typing | With the behavior: move to the next item that starts with the typed letters |
| ContextMenu, Shift+F10 | With contextMenu() from the behavior: open a menu-context at the focused element, as a right click and a long press do at the pointer |
#Accessibility
- Without script, a menu is a popover of buttons and links: Tab moves through the items, Esc closes it and focus returns to the button. That is complete and correct as it stands, so do not add
role="menu". role="menu"androle="menuitem"promise arrow-key navigation. Add them only together with themenubehavior, or with a script of your own that provides the same keys.- Give an icon-only button an accessible name with
aria-label.
#React, Svelte and Vue
The menu component renders the items from data, joins the menu to its trigger and attaches the behavior. Given a target instead of a trigger, it is a context menu. Copy it into the project with npx master-ui add menu; React, Svelte and Vue says how the components are built and used.
#Classes
| Class | Description |
|---|---|
menu | The floating list, on an element with the popover attribute; without the attribute it is never shown. It opens below its button, lined up with the start edge, at a distance of --anchored-gap (6px), and flips at the viewport edge |
menu-end | Lines up with the button's end edge, for a button at the end of a row or a toolbar. With menu |
menu-top | Opens above the button. With menu |
menu-sub | A submenu: written right after the item that opens it, inside the parent menu, it opens beside that item. With menu |
menu-context | A context menu: opens at --menu-x and --menu-y instead of against a button. With menu |
menu-item | An action: a button, a link, or a label around a menu-input. An icon inside takes the item's size and stays quiet until the row is in use |
menu-item-danger | A destructive action, in the danger ink. With menu-item |
menu-input | The checkbox or radio inside a label item. It covers the row invisibly and keeps the state; the row shows a check at its end |
menu-shortcut | A shortcut at the end of an item |
menu-label | A quiet caption over a group of items |
menu-separator | A hairline between groups, on an hr, running to the menu's edges |
#Custom properties
| Property | Default | Description |
|---|---|---|
--menu-inset | 4px | Padding around the items |
--menu-item-radius | var(--radius-control-sm) | Row corner; the menu's corner is this plus the inset |
--menu-x | Distance from the left edge of the viewport where a context menu opens, set by script from the event; 0px when not set. On menu-context | |
--menu-y | Distance from the top edge of the viewport where a context menu opens, set by script from the event; 0px when not set. On menu-context | |
--anchored-gap | Space between the surface and the control that opened it; 6px when not set. On menu |