Primitives Overlays
Context menu
A menu that opens where the user asks for it: a secondary click, a long press, the context-menu key or Shift+F10. The behavior takes the browser's contextmenu event and opens a menu popover at that point; the menu contract does the rest. Tier 1.
- Tier
- 1 · Delegated micro-behavior
- Behavior JS
- 1409 B brotli · 1574 B gzip ·
packages/· methodprimitives/ artifacts/ size.json - Platform features
contextmenu,showPopover(),role="menu",Shift+F10- Shared listeners
- contextmenu
- Per-instance listeners
- none
- Lazy state
- none
- Native base
contextmenu event + [popover] ARIA menu
On this page
Example
- report.pdf
- budget.xlsx
- roadmap.md
- logo.svg
Right-click a file, or focus the list and press Shift+F10.
Why this implementation exists
Every way of asking for a context menu already arrives as one event: contextmenu is dispatched for a secondary click, for the context-menu key and Shift+F10, and for a long press where the platform supports it. What the browser shows by default is its own menu. A context menu replaces it: cancel the event, open a menu popover at the pointer.
After the opening there is nothing new to build. The menu is the Menu primitive: a popover in the top layer with light dismiss and Escape from the platform, and arrows, typeahead and activation from the menu behavior. The context menu adds one shared listener and the placement, which keeps the menu inside the viewport with one layout read per opening.
The browser owns
- the contextmenu event for a secondary click, a long press, the context-menu key and Shift+F10
- the top layer, light dismiss, Escape and focus restoration of the menu popover
- the browser’s own menu wherever the request is not taken
Svelte Lean owns
- opening the menu at the pointer, or under the focused element for a keyboard request
- keeping the menu inside the viewport; RTL placement
- the cancelable slean:open with the element the menu was requested on
- the menu behavior: arrows, typeahead, activation, checked items (the menu contract)
- context-menu.css: the target cursor and the menu origin, with popover.css and menu.css
Usage
Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the
registration is injected for every static data-slean="context-menu"; without it, import
the register module once.
import '@svelte-lean/primitives/context-menu/register';import '@svelte-lean/styles/popover.css';
import '@svelte-lean/styles/menu.css';
import '@svelte-lean/styles/context-menu.css';Wrap the area and a menu root: target on the area (focusable, so the keyboard can ask for the menu) and menu on a data-slean="menu" popover named with aria-label.
Listen to slean:open to learn which element the menu was requested on, for example the row or the file under the pointer, and fill or adjust the menu there. Cancelling it lets the browser show its own menu.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div data-slean="context-menu"> | – | yes | No options. |
| target | <div tabindex="0"> | target | yes | The area that has the menu; focusable so the keyboard can request it. |
| menu | <div popover role="menu" aria-label="…" data-slean="menu"> | menu | yes | A menu root with its own contract. |
Runtime profile
The context menu registers one contextmenu handler with the shared router; the menu registers its own. Opening reads the menu's box once to keep it inside the viewport. A thousand targets keep one listener (tests/context-menu.test.ts).
Accessibility contract
- The menu is an ARIA menu named by
aria-label; its keyboard is the menu contract. - Shift+F10 and the context-menu key open it under the focused element, so the menu is reachable without a pointer.
- Opening moves focus to the first item; Escape or a choice returns focus to the element that had it.
- A context menu is hidden by nature: offer the same actions somewhere visible, a toolbar or a row menu, for people who never right-click.
Keyboard
| Key | When | Result |
|---|---|---|
| Shift+F10/ContextMenu | focus in the target | Opens the menu under the focused element |
| ArrowDown/ArrowUp/Enter/Escape | the menu is open | The menu contract: move, choose, close |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
contextmenu event | Widely available | Safari on iOS does not dispatch it for a long press |
Popover API | Baseline 2024 (Chrome 114, Safari 17, Firefox 125) | The menu never opens; the browser menu stays |
Without JavaScript
The browser's own context menu appears; the menu popover never opens.
Server rendering
Render the markup as is; the menu is closed and costs no layout.
Before hydration
Before the behavior loads, a right click shows the browser's menu. The first request after hydration opens the menu.
Styling
context-menu.css sets the target's cursor and the menu's transform origin, and removes the anchor placement so the inline position applies; the surface and the rows come from popover.css and menu.css. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-radius-md | 0.625rem | border-radius |
State selectors the stylesheet targets, all from the platform or ARIA: :dir(rtl).
Compatibility notes
The contextmenu event is widely available, but Safari on iOS does not dispatch it for a long press: touch users of an iPhone or iPad cannot open the menu. The Popover API is Baseline 2024.
Testing
packages/primitives/tests/context-menu.test.tsVitest: pointer and keyboard placement, viewport, RTL, slean:open, 1000 rootsapps/playground/tests/primitives/layout.spec.tsPlaywright: the splitter and the context menu in Chrome, with axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/context-menu/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/context-menu/contract.tstyped constants: name, tier, base, parts, options, eventspackages/primitives/src/context-menu/behavior.tsthe behavior definitionpackages/primitives/src/context-menu/register.tsthe registration modulepackages/styles/css/context-menu.cssthe optional stylesheet