Navigation

v0.0.4
Overlays

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 popovertarget and popovertargetaction="hide", as above; leave them out to keep the menu open.
  • menu-item-danger gives a destructive action the danger ink.
  • menu-shortcut sets 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 the href.

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

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>
Open the menu and use the arrow keys, Home, End or a letter. The right arrow on Share opens its submenu and the left arrow closes it.

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.

app.js
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 a menu-sub follows, 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 click on an item and change on a menu-input.
  • Roles. The behavior adds none. Write role="menu" on the menu and role="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.

app.js
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.

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

KeysAction
Enter, SpaceOn the button with popovertarget: open or close the menu. On an item: run it, or open its submenu
Tab, Shift+TabWithout the behavior: move through the items. With the behavior: close the menu and move on from its button
EscapeClose the menu, one level at a time when a submenu is open, and return focus to the button
ArrowDown, ArrowUpWith the behavior: move to the next or the previous item, skipping disabled ones and wrapping at the ends
Home, EndWith the behavior: move to the first or the last item
ArrowRight, ArrowLeftWith 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
TypingWith the behavior: move to the next item that starts with the typed letters
ContextMenu, Shift+F10With 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" and role="menuitem" promise arrow-key navigation. Add them only together with the menu behavior, 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.

ui/menu.tsx
'use client'import { useEffect, useId, useRef, type ComponentProps, type ReactNode, type RefObject } from 'react'import { contextMenu, menu } from '@master/ui/behaviors/menu.js'export type MenuItem =  | { label: ReactNode, shortcut?: ReactNode, href?: string, onSelect?: () => void, danger?: boolean, disabled?: boolean }  | { separator: true }export interface MenuProps extends Omit<ComponentProps<'div'>, 'children'> {  items: MenuItem[]  /** The button that opens the menu: spread what it is given on a button of your own. */  trigger?: (attributes: { popoverTarget: string, 'aria-haspopup': 'menu' }) => ReactNode  /** Instead of a button: the element the menu belongs to. It opens at the pointer on a right click or a long press. */  context?: RefObject<HTMLElement | null>}/** A menu of actions with the keys of a menu. Name it with `aria-label`. */export function Menu({ items, trigger, context, className, id, ...root }: MenuProps) {  const generated = useId()  const name = id ?? generated  const element = useRef<HTMLDivElement>(null)  useEffect(() => {    const detach = menu(element.current!)    const target = context?.current    const forget = target ? contextMenu(target, element.current!) : undefined    return () => {      detach()      forget?.()    }  }, [context])  return (    <>      {trigger?.({ popoverTarget: name, 'aria-haspopup': 'menu' })}      <div id={name} popover="auto" className={['menu', context && 'menu-context', className].filter(Boolean).join(' ')} role="menu" ref={element} {...root}>        {items.map((item, index) => 'separator' in item          ? <hr className="menu-separator" key={index} />          : item.href            ? <a className={item.danger ? 'menu-item menu-item-danger' : 'menu-item'} role="menuitem" key={index} href={item.href} aria-disabled={item.disabled || undefined}>{item.label}{item.shortcut !== undefined && <span className="menu-shortcut">{item.shortcut}</span>}</a>            : <button type="button" className={item.danger ? 'menu-item menu-item-danger' : 'menu-item'} role="menuitem" key={index} popoverTarget={name} popoverTargetAction="hide" onClick={item.onSelect} disabled={item.disabled}>{item.label}{item.shortcut !== undefined && <span className="menu-shortcut">{item.shortcut}</span>}</button>)}      </div>    </>  )}
ui/Menu.svelte
<script lang="ts" module>  import type { Snippet } from 'svelte'  import type { HTMLAttributes } from 'svelte/elements'  export type MenuItem =    | { label: string, shortcut?: string, href?: string, onSelect?: () => void, danger?: boolean, disabled?: boolean }    | { separator: true }  export interface MenuProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {    items: MenuItem[]    /** The button that opens the menu: spread what it is given on a button of your own. */    trigger?: Snippet<[{ popovertarget: string, 'aria-haspopup': 'menu' }]>    /**     * Instead of a button: the element the menu belongs to, as `bind:this` gives it. The menu opens at the     * pointer on a right click or a long press. Null stands for an element that is not there yet.     */    context?: HTMLElement | null  }</script><script lang="ts">  import type { Attachment } from 'svelte/attachments'  import { contextMenu, menu } from '@master/ui/behaviors/menu.js'  let { items, trigger, context, class: className, id, ...root }: MenuProps = $props()  const generated = $props.id()  const name = $derived(id ?? generated)  // Attached again once the element the menu belongs to is there, and when it is another.  const attach: Attachment<HTMLDivElement> = (element) => {    const detach = menu(element)    const forget = context ? contextMenu(context, element) : undefined    return () => {      detach()      forget?.()    }  }</script><!--  @component  A menu of actions with the keys of a menu. Name it with `aria-label`.-->{@render trigger?.({ popovertarget: name, 'aria-haspopup': 'menu' })}<div id={name} popover="auto" class={['menu', context !== undefined && 'menu-context', className]} role="menu" {@attach attach} {...root}>  {#each items as item}    {#if 'separator' in item}      <hr class="menu-separator" />    {:else if item.href}      <a class={item.danger ? 'menu-item menu-item-danger' : 'menu-item'} role="menuitem" href={item.href} aria-disabled={item.disabled || undefined}>{item.label}{#if item.shortcut !== undefined}<span class="menu-shortcut">{item.shortcut}</span>{/if}</a>    {:else}      <button type="button" class={item.danger ? 'menu-item menu-item-danger' : 'menu-item'} role="menuitem" popovertarget={name} popovertargetaction="hide" onclick={item.onSelect} disabled={item.disabled}>{item.label}{#if item.shortcut !== undefined}<span class="menu-shortcut">{item.shortcut}</span>{/if}</button>    {/if}  {/each}</div>
ui/Menu.vue
<script lang="ts">export type MenuItem =  | { label: string, shortcut?: string, href?: string, onSelect?: () => void, danger?: boolean, disabled?: boolean }  | { separator: true }</script><script setup lang="ts">import { computed, useId, useTemplateRef, watchEffect } from 'vue'import { contextMenu, menu } from '@master/ui/behaviors/menu.js'defineOptions({ inheritAttrs: false })const { id, context } = defineProps<{  items: MenuItem[]  id?: string  /**   * Instead of a button: the element the menu belongs to, null until it is mounted. The menu opens at the   * pointer on a right click or a long press.   */  context?: HTMLElement | null}>()defineSlots<{  /** The button that opens the menu: bind what it is given on a button of your own. */  trigger?: (attributes: { 'popovertarget': string, 'aria-haspopup': 'menu' }) => unknown}>()/* A menu of actions with the keys of a menu. Name it with `aria-label`. */const generated = useId()const name = computed(() => id ?? generated)const root = useTemplateRef<HTMLDivElement>('root')watchEffect((onCleanup) => {  const element = root.value  if (!element) return  const detach = menu(element)  const forget = context ? contextMenu(context, element) : undefined  onCleanup(() => {    detach()    forget?.()  })}, { flush: 'post' })</script><template>  <slot name="trigger" v-bind="{ 'popovertarget': name, 'aria-haspopup': 'menu' as const }" />  <div :id="name" popover="auto" class="menu" :class="{ 'menu-context': context !== undefined }" role="menu" ref="root" v-bind="$attrs">    <template v-for="(item, index) in items" :key="index">      <hr v-if="'separator' in item" class="menu-separator">      <a v-else-if="item.href" class="menu-item" :class="{ 'menu-item-danger': item.danger }" role="menuitem" :href="item.href" :aria-disabled="item.disabled || undefined">{{ item.label }}<span v-if="item.shortcut !== undefined" class="menu-shortcut">{{ item.shortcut }}</span></a>      <button v-else type="button" class="menu-item" :class="{ 'menu-item-danger': item.danger }" role="menuitem" :popovertarget="name" popovertargetaction="hide" :disabled="item.disabled" @click="item.onSelect?.()">{{ item.label }}<span v-if="item.shortcut !== undefined" class="menu-shortcut">{{ item.shortcut }}</span></button>    </template>  </div></template>

#Classes

ClassDescription
menuThe 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-endLines up with the button's end edge, for a button at the end of a row or a toolbar. With menu
menu-topOpens above the button. With menu
menu-subA submenu: written right after the item that opens it, inside the parent menu, it opens beside that item. With menu
menu-contextA context menu: opens at --menu-x and --menu-y instead of against a button. With menu
menu-itemAn 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-dangerA destructive action, in the danger ink. With menu-item
menu-inputThe 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-shortcutA shortcut at the end of an item
menu-labelA quiet caption over a group of items
menu-separatorA hairline between groups, on an hr, running to the menu's edges

#Custom properties

PropertyDefaultDescription
--menu-inset4pxPadding around the items
--menu-item-radiusvar(--radius-control-sm)Row corner; the menu's corner is this plus the inset
--menu-xDistance 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-yDistance 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-gapSpace between the surface and the control that opened it; 6px when not set. On menu

#Source

@master/ui/components/menu.css
/* * Menu: a popover holding a list of actions, placed against its button with anchor positioning. * Items are native buttons, links, or labels around a checkbox or radio; the row under the pointer or the keys * takes a chip tone. The menu's corner is a row's corner plus its 4px inset. A menu can also open beside an * item as a submenu, or at the pointer as a context menu. */@layer components {  .menu {    --menu-inset: 4px;    --menu-item-radius: var(--radius-control-sm);    --surface-color: var(--color-surface-floating);    min-width: 11rem;    max-width: min(20rem, calc(100vw - 1rem));    padding: var(--menu-inset);    border: 0;    border-radius: calc(var(--menu-item-radius) * var(--corner-scale) + var(--menu-inset));    corner-shape: var(--corner-shape);    overflow-y: auto;    background: var(--color-surface-floating);    color: var(--color-text-strong);    box-shadow: var(--shadow-floating);    font: 400 .84375rem/1.2 var(--font-family-sans);    scrollbar-width: thin;    transform-origin: top;    @apply --anchored;    @apply --pop;    overscroll-behavior: contain;    /* Line up with the button's end edge, for a menu opened from the end of a row. */    &.menu-end {      --anchored-inline: span-inline-start;    }    /* Open above the button. */    &.menu-top {      --anchored-block: block-start;      transform-origin: bottom;    }    /*     * Beside its item: a submenu, opened by an item inside another menu. It lines up with the item's top,     * 4px up so its first row meets the item, and flips to the other side when there is no room.     */    &.menu-sub {      --anchored-block: span-block-end;      --anchored-inline: inline-end;      transform-origin: top left;      @supports (position-area: block-end) {        margin: calc(-1 * var(--menu-inset)) 2px;      }    }    /*     * At the pointer: a context menu. The script sets --menu-x and --menu-y from the event and calls     * showPopover(). The menu opens down and to the end of that point, and @position-try turns it back     * toward the middle when it would cross an edge of the viewport.     */    &.menu-context {      inset: var(--menu-y, 0px) auto auto var(--menu-x, 0px);      margin: 0;      position-area: none;      position-try-fallbacks: --menu-context-start, --menu-context-up, --menu-context-up-start;      transform-origin: top left;    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;    }  }  /* An action: a button, a link, or a label around a .menu-input checkbox or radio. */  .menu-item {    --icon-size: 1rem;    --icon-offset: -.0625rem;    --icon-color: var(--color-text-muted);    position: relative;    display: flex;    align-items: center;    gap: .625rem;    width: 100%;    min-height: 2rem;    margin: 0;    padding: 0 .5rem;    border: 0;    border-radius: calc(var(--menu-item-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    outline: none;    background: transparent;    color: var(--color-text-strong);    font: inherit;    font-feature-settings: inherit;    text-align: start;    text-decoration: none;    white-space: nowrap;    appearance: none;    user-select: none;    -webkit-user-select: none;    -webkit-tap-highlight-color: transparent;    cursor: pointer;    touch-action: manipulation;    /* A row answers at once: no fade between rows as the pointer travels. */    @media (hover: hover) {      &:hover:not(:disabled, [aria-disabled="true"], :has(.menu-input:disabled)) {        --icon-color: currentColor;        background: var(--color-surface-chip);      }    }    &:active:not(:disabled, [aria-disabled="true"], :has(.menu-input:disabled)) {      background: var(--color-surface-inset-hover);    }    /* The keys leave an ink outline inside the row, so focus and hover stay different facts. */    &:is(:focus-visible, :has(.menu-input:focus-visible)) {      --icon-color: currentColor;      background: var(--color-surface-chip);      outline: 2px solid var(--color-focus);      outline-offset: -2px;    }    &:is(:disabled, [aria-disabled="true"], :has(.menu-input:disabled)) {      --icon-color: currentColor;      color: var(--color-text-disabled);      cursor: not-allowed;    }    /* A destructive action carries the danger ink and a faint tint of it under the pointer. */    &.menu-item-danger:not(:disabled, [aria-disabled="true"], :has(.menu-input:disabled)) {      --icon-color: currentColor;      color: var(--color-danger);      @media (hover: hover) {        &:hover {          background: color-mix(in oklab, var(--color-danger) 9%, transparent);        }      }      &:focus-visible {        background: color-mix(in oklab, var(--color-danger) 9%, transparent);      }    }    /* An item that opens a submenu points to it, and stays lit while that submenu is open. */    &[aria-haspopup="menu"]::after {      content: '';      flex: none;      width: .875rem;      height: .875rem;      margin-inline: auto -.125rem;      background: var(--color-text-muted);      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6.5 4.5 3.5 3.5-3.5 3.5'/%3E%3C/svg%3E") center / contain no-repeat;    }    &[aria-haspopup="menu"]:dir(rtl)::after {      scale: -1 1;    }    &:has(+ .menu:popover-open) {      --icon-color: currentColor;      background: var(--color-surface-chip);    }    /* A checkable item keeps room for its mark at the end, so labels do not move when it is checked. */    &:is([aria-checked], :has(.menu-input))::after {      content: '';      flex: none;      width: 14px;      height: 14px;      margin-inline-start: auto;      opacity: 0;      background: currentColor;      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-7'/%3E%3C/svg%3E") center / contain no-repeat;    }    /* Beside a shortcut, the mark follows it: only one of the two is pushed to the end of the row. */    &:is([aria-checked], :has(.menu-input)):has(.menu-shortcut)::after {      margin-inline-start: 0;    }    &:is([aria-checked="true"], :has(.menu-input:checked))::after {      opacity: 1;    }    @media (pointer: coarse) {      min-height: 2.75rem;    }    @media (forced-colors: active) {      &:is(:hover, :focus-visible, :has(.menu-input:focus-visible)):not(:disabled, [aria-disabled="true"], :has(.menu-input:disabled)) {        background: Highlight;        color: HighlightText;        forced-color-adjust: none;      }      &::after {        background: CanvasText;      }    }  }  /* The checkbox or radio inside an item covers it invisibly: the label is the target, the input keeps the state. */  .menu-input {    position: absolute;    inset: 0;    width: 100%;    height: 100%;    margin: 0;    padding: 0;    border: 0;    border-radius: inherit;    opacity: 0;    appearance: none;    cursor: inherit;  }  /*   * In a menu with checkable items, every other row keeps the mark's room too, so the shortcuts of checked   * and plain rows stand in one column.   */  .menu:has(.menu-item[aria-checked], .menu-input) .menu-item:not([aria-checked], [aria-haspopup="menu"], :has(.menu-input))::after {    content: '';    flex: none;    width: 14px;  }  /* A shortcut at the end of an item. */  .menu-shortcut {    margin-inline-start: auto;    padding-inline-start: 1.5rem;    color: var(--color-text-muted);    font: 400 .71875rem/1 var(--font-family-sans);    letter-spacing: .02em;  }  /* A quiet caption over a group of items. */  .menu-label {    padding: .4375rem .5rem .3125rem;    color: var(--color-text-muted);    font-size: .71875rem;    user-select: none;    -webkit-user-select: none;  }  /* A hairline between groups, running to the menu's edges. */  .menu-separator {    height: 1px;    margin: var(--menu-inset) calc(-1 * var(--menu-inset));    border: 0;    background: var(--color-line-hair);  }}/* The other three corners a context menu can open toward, measured from the same point. */@position-try --menu-context-start {  right: calc(100vw - var(--menu-x, 0px));  left: auto;}@position-try --menu-context-up {  top: auto;  bottom: calc(100vh - var(--menu-y, 0px));}@position-try --menu-context-up-start {  top: auto;  right: calc(100vw - var(--menu-x, 0px));  bottom: calc(100vh - var(--menu-y, 0px));  left: auto;}