sveltelean Primitives
Versionv0.2.0 GitHub

Example

File actions
  • report.pdf
  • budget.xlsx
  • roadmap.md
  • logo.svg

Right-click a file, or focus the list and press Shift+F10.

<div data-slean="context-menu">
	<ul data-slean-part="target" tabindex="0" aria-label="Files">
		<li tabindex="-1">report.pdf</li>
		<li tabindex="-1">budget.xlsx</li>
	</ul>
	<div id="context-files" popover role="menu" aria-label="File actions" data-slean="menu"
		data-slean-part="menu">
		<button type="button" role="menuitem" data-slean-value="open">Open</button>
		<button type="button" role="menuitem" data-slean-value="rename">Rename</button>
		<hr />
		<button type="button" role="menuitem" data-slean-value="delete">Delete</button>
	</div>
</div>
Right-click a file. From the keyboard, focus the list and press Shift+F10 or the context-menu key: the menu opens under the focused file. Escape closes it and focus returns.

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.

npm install @svelte-lean/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/context-menu/register';
stylesheets
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

PartElementdata-slean-partRequiredNotes
root<div data-slean="context-menu">–yesNo options.
target<div tabindex="0">targetyesThe area that has the menu; focusable so the keyboard can request it.
menu<div popover role="menu" aria-label="…" data-slean="menu">menuyesA 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

KeyWhenResult
Shift+F10/ContextMenufocus in the targetOpens the menu under the focused element
ArrowDown/ArrowUp/Enter/Escapethe menu is openThe menu contract: move, choose, close

Platform features

FeatureBaselineOutside the target
contextmenu eventWidely availableSafari on iOS does not dispatch it for a long press
Popover APIBaseline 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:

TokenDefault (light)Applies to
--slean-radius-md0.625remborder-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

Source