# Context Menu

Open a menu at the pointer, or from the keyboard. (Custom element.)

Reference: https://timeless.build/docs/components/context-menu/

> **Choosing between components.** A context menu is the same surface as [Menu](/docs/components/menu/), opened by a secondary click over a region instead of by a control. Reach for [Menu Button](/docs/components/menu-button/) when the commands should have a visible trigger — a context menu is discoverable only by trying it, so it belongs to shortcuts for things reachable another way.

> **Markup you author.** Never put a command here that is not also reachable elsewhere. Give the `target` a role that supports `aria-haspopup` and an accessible name — in real markup it is usually something that already has both, like a table row or a treeitem — and Timeless adds the tab stop, the relationships, and the `Shift+F10` and Context Menu key routes in.

## Markup

```html
<ui-context-menu>
  <div data-ui-part="target" role="group" aria-label="hero-banner.avif">
    hero-banner.avif
  </div>
  <ui-menu id="asset-context-menu" role="menu" popover="auto" aria-label="Asset commands">
    <button role="menuitem" type="button">Open</button>
    <button role="menuitem" type="button">Rename</button>
    <hr data-ui-part="separator" role="separator">
    <div data-ui-part="group">
      <p data-ui-part="group-label">Share</p>
      <button role="menuitem" type="button">Copy link</button>
      <button role="menuitem" type="button">Invite reviewer</button>
    </div>
  </ui-menu>
</ui-context-menu>
```

## Install

```js
import '@timelessui/components/css/tokens.css'
import '@timelessui/components/css/core/context-menu.css'
import '@timelessui/components/css/core/floating.css'
import '@timelessui/components/css/core/menu.css'
import '@timelessui/components/css/core/button.css'
import '@timelessui/components/css/themes/atmosphere/tokens.css'
import '@timelessui/components/css/themes/atmosphere/button.css'
import '@timelessui/components/css/themes/atmosphere/menu.css'
import '@timelessui/components/css/themes/atmosphere/context-menu.css'
import '@timelessui/components/register/ui-context-menu'
import '@timelessui/components/register/ui-menu'
```

A `register/` import is a side effect: it defines the element as the module evaluates, so it
has to run in code the browser loads. To register explicitly instead — to control the timing,
or to define into another window — call the function from the matching `define/` entry point,
which registers nothing on its own:

```js
import { defineContextMenuElement } from '@timelessui/components/define/ui-context-menu'
defineContextMenuElement()
import { defineMenuElement } from '@timelessui/components/define/ui-menu'
defineMenuElement()
```

## Anatomy

Parts are authored in your own markup and identified by the selector below. Required parts
must be present for the component to work. Private `data-ui-internal-*` hooks are written by
the runtime and must never be authored.

**<ui-context-menu>**

| Part | Required | Selector | Purpose |
| --- | --- | --- | --- |
| `target` | Yes | `[data-ui-part~='target']` | The region a secondary click opens the menu over. Give it a role that supports `aria-haspopup` and make it focusable — Timeless adds `tabindex="0"` when it has none, because the keyboard path cannot exist without a tab stop. It then wires `aria-haspopup`, `aria-controls`, and `aria-expanded`; the role and the accessible name stay yours. |
| `menu` | Yes | `ui-menu[popover], [role='menu'][popover]` | The menu surface, a `ui-menu` authored as a popover. Every item, group, separator, and submenu inside it is the [Menu](/docs/components/menu/) contract, unchanged — this element only decides when and where it opens. |

**<ui-menu>**

| Part | Required | Selector | Purpose |
| --- | --- | --- | --- |
| `menu` | Yes | `[role='menu']` | The menu container. Use `role="menubar"` for a persistent horizontal menu bar. |
| `item` | Yes | `[role^='menuitem']` | One command. A menu-item role is what makes an element an item — a bare `<button>` inside the menu is not one. Use `role="menuitem"`, or `menuitemcheckbox` / `menuitemradio` for a checkable command. Timeless manages roving `tabindex`, typeahead, and `aria-checked`. |
| `group` | No | `[data-ui-part~='group']` | A `role="group"` wrapper around related items. Items inside it stay navigable, and a `menuitemradio` clears only the radios in its own group. |
| `group-label` | No | `[data-ui-part~='group-label']` | The label for a `group`, wired to it with `aria-labelledby`. |
| `separator` | No | `[role='separator'], hr` | A divider between items or groups. Navigation and typeahead skip it, because it carries no menu-item role. |
| `submenu-trigger` | No | `[aria-haspopup='menu']` | An item that owns a submenu. You do not author this token: give the item `aria-controls` naming the submenu, or put the submenu immediately after it, and Timeless writes `aria-haspopup`, `aria-controls`, and `aria-expanded`. |
| `submenu` | No | `ui-menu[popover], [role='menu'][popover]` | A nested menu opened from a `submenu-trigger`. Author it as a popover so it stays hidden before enhancement; Timeless adds `popover="auto"` if you leave it off. |

## Attributes

Every value below is implemented by the stylesheets this component ships. Boolean attributes
are presence-based: author the attribute with no value, or omit it.

**<ui-menu>**

| Attribute | Values | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `horizontal` · `vertical` | `vertical` | Arrow-key axis. Defaults to `horizontal` when the menu part is `role="menubar"`. |

## Element API

Attributes above are the authoring surface. These are the DOM properties and events the
registered element adds once it upgrades.

### `<ui-context-menu>`

This element adds no public properties. Configure it with its attributes.

| Event | Detail | Description |
| --- | --- | --- |
| `ui-open` | `CustomEvent<ContextMenuToggleDetail>` | Dispatched after the context menu opens. |
| `ui-close` | `CustomEvent<ContextMenuToggleDetail>` | Dispatched after the context menu closes. |

### `<ui-menu>`

| Property | Type | Notes |
| --- | --- | --- |
| `orientation` | `'horizontal' | 'vertical'` | Reflects the `orientation` attribute |

| Event | Detail | Description |
| --- | --- | --- |
| `ui-before-change` | `CustomEvent<MenuCheckedDetail>` | Cancelable proposal dispatched before a checkable item changes. Call `preventDefault()` to reject the transition and keep the current value. |
| `ui-change` | `CustomEvent<MenuCheckedDetail>` | Dispatched after a checkable item has changed. Bubbles and is composed. |

## Styling

Root identity: `ui-context-menu`, `ui-menu`.
Required stylesheets: `tokens.css`, `core/context-menu.css`, `core/floating.css`, `core/menu.css`, `core/button.css`, `themes/atmosphere/tokens.css`, `themes/atmosphere/button.css`, `themes/atmosphere/menu.css`, `themes/atmosphere/context-menu.css`.

Set these custom properties to restyle the component:

| Custom property | Controls |
| --- | --- |
| `--ui-context-menu-x` | Horizontal position the surface opens at, written at runtime from the pointer or from the focused element. The stylesheet turns it into a clamped `left`. |
| `--ui-context-menu-y` | Vertical position, the same way. Together these are the whole positioning input: there is no anchor element, because a pointer is not an element. |
| `--ui-context-menu-inset` | Minimum gap kept between the surface and the viewport edge when the coordinates would push it off screen. |
| `--ui-menu-min-inline-size` | Minimum width of the menu surface. |

Design tokens this component reads (32), global and set at the theme level: `--ui-accent`, `--ui-accent-soft`, `--ui-bg-accent`, `--ui-bg-accent-active`, `--ui-bg-accent-hover`, `--ui-bg-control`, `--ui-bg-control-active`, `--ui-bg-control-hover`, `--ui-bg-control-muted`, `--ui-bg-danger`, `--ui-bg-danger-active`, `--ui-bg-danger-hover`, `--ui-bg-surface`, `--ui-bg-surface-raised`, `--ui-disabled-opacity`, `--ui-duration-fast`, `--ui-ease-standard`, `--ui-fg`, `--ui-fg-muted`, `--ui-focus`, `--ui-line`, `--ui-radius-control`, `--ui-radius-lg`, `--ui-radius-sm`, `--ui-shadow-control-accent`, `--ui-shadow-control-active`, `--ui-shadow-control-danger`, `--ui-shadow-floating`, `--ui-shadow-outline-control`, `--ui-space-1`, `--ui-space-2`, `--ui-space-3`.

## Accessibility

Keep the native elements, roles, and relationships shown in the markup. Timeless adds only the
state and keyboard coordination the platform does not already provide, and it never supplies
your accessible names — those depend on your content.

**<ui-context-menu>**

The ARIA Authoring Practices Guide has no pattern for Context Menu. The contract below is a composition of behavior the platform already defines, not ARIA invented to fill the gap.

| Key | Action |
| --- | --- |
| `Shift + F10` | Open the menu for the focused target. Some environments consume the shortcut before the page sees it, which is why the dedicated key below is also supported. |
| `Context Menu key` | Open the menu for the focused target. |
| `Escape` | Close the menu and return focus to the target. Handled by the Popover API and by Menu. |

The APG has no context-menu pattern, so this documents a composition rather than claiming one: a [Menu](/docs/components/menu/) surface, opened by a secondary click or by the keyboard. Everything inside is the menu pattern — roving focus, typeahead, submenu keys, checkable items. Escape, light dismiss, and top-layer stacking come from the Popover API. **This is the one Timeless component with no no-JavaScript fallback**: the platform has no declarative way to open a surface at pointer coordinates, so with scripting off the browser shows its own context menu and the authored `ui-menu` stays hidden. Never put a command here that is not also reachable another way.

**<ui-menu>**

Follows the [Menu and Menubar pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) from the ARIA Authoring Practices Guide.

| Key | Action |
| --- | --- |
| `Arrow keys` | Move focus between items along the `orientation` axis. |
| `Home / End` | Move focus to the first or last item. |
| `Enter / Space` | Activate the focused item, or open its submenu. |
| `Arrow Right` | Open the focused item's submenu and focus its first enabled item. Under `dir="rtl"` this is Arrow Left. |
| `Arrow Left` | Close the submenu and return focus to the item that opened it. From a first-level submenu of a menubar, move along the bar instead. Under `dir="rtl"` this is Arrow Right. |
| `Escape` | Close the menu and return focus to whatever opened it. |
| `Printable characters` | Typeahead: jump to the next item whose label starts with what you typed. |

The menu is one tab stop. Mark an unavailable command `aria-disabled="true"` rather than `disabled`: it stays reachable with the arrow keys, which is the APG treatment — a command you cannot use is easier to understand than one that is not there — while never taking the resting tab stop and never activating. An authored `disabled` item is honoured as written and simply skipped, because the platform makes it unfocusable and Timeless does not rewrite your markup. Activating a `menuitemcheckbox` toggles its `aria-checked`; activating a `menuitemradio` sets it and clears the other radios in its group. Both dispatch a cancelable `ui-before-change` first, so a consumer that already owns `aria-checked` can keep owning it. Typeahead matching is locale-aware. Submenus open on the keyboard and on click; there is deliberately no hover-with-intent opening.

## Before JavaScript runs

The markup above is complete and usable on its own: native controls submit, labels associate, and authored ARIA is already correct. Registration adds state synchronisation, focus management, and keyboard coordination on top of it.
