ContextMenu
Import
Section titled “Import”import { contextMenu } from '@delightstack/components';Also import the ContextMenu component itself (typically in your root layout) to mount the global context menu renderer:
import { ContextMenu } from '@delightstack/components';Basic Usage
Section titled “Basic Usage”Attach a context menu to any element using {@attach contextMenu()}. The ContextMenu component must be mounted somewhere in your component tree (usually in your root layout).
View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components';</script>
<div {@attach contextMenu({ actions: [ { label: 'Edit', onclick: () => editItem() }, { label: 'Duplicate', onclick: () => duplicateItem() }, { label: 'Delete', onclick: () => deleteItem() }, ]})}> Right-click in this area</div>
<ContextMenu /><script> import { contextMenu, ContextMenu } from '@delightstack/components';</script>
<div {@attach contextMenu({ actions: [ { label: 'Edit', onclick: () => editItem() }, { label: 'Delete', onclick: () => deleteItem() } ]})}> Right-click me</div>
<!-- Mount once in your layout --><ContextMenu />Examples
Section titled “Examples”With Icons
Section titled “With Icons”Pass a Svelte component to an action’s icon property to render it before the label.
View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components'; import { EditIcon, CopyIcon, DeleteIcon } from './icons';
let lastAction = $state('');</script>
<div {@attach contextMenu({ actions: [ { label: 'Edit', icon: EditIcon, onclick: () => lastAction = 'Edit clicked' }, { label: 'Copy', icon: CopyIcon, onclick: () => lastAction = 'Copy clicked' }, { label: 'Delete', icon: DeleteIcon, onclick: () => lastAction = 'Delete clicked' }, ]})}> Right-click for file options</div>
<ContextMenu />With Links
Section titled “With Links”Actions can navigate using href and target instead of onclick.
View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components';</script>
<div {@attach contextMenu({ actions: [ { label: 'View Profile', href: '#profile' }, { label: 'Open in New Tab', href: '#profile', target: '_blank' }, ]})}> Right-click for navigation options</div>
<ContextMenu />With Active State and Disabled Items
Section titled “With Active State and Disabled Items”View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components';
let viewMode = $state('grid');</script>
<div {@attach contextMenu({ actions: [ { label: 'Grid View', active: viewMode === 'grid', onclick: () => viewMode = 'grid' }, { label: 'List View', active: viewMode === 'list', onclick: () => viewMode = 'list' }, { label: 'Table View', disabled: true }, ]})}> Right-click to change view (current: {viewMode})</div>
<ContextMenu />Custom Rendered Items
Section titled “Custom Rendered Items”Set an action’s snippet property to render custom content instead of a plain label. Each action renders its own snippet, so you can compose icons, secondary text, shortcut hints, or any markup you like.
View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components';
let lastAction = $state('');</script>
<div {@attach contextMenu({ actions: [ { snippet: emailItem, onclick: () => lastAction = 'Shared via email' }, { snippet: linkItem, onclick: () => lastAction = 'Link copied' }, { snippet: embedItem, onclick: () => lastAction = 'Embed code copied' }, ]})}> Right-click to share</div>
{#snippet row(emoji, title, hint)} <span class="row"> <span>{emoji}</span> <span class="title">{title}</span> <span class="hint">{hint}</span> </span>{/snippet}
{#snippet emailItem()}{@render row('✉️', 'Email', '⌘E')}{/snippet}{#snippet linkItem()}{@render row('🔗', 'Copy link', '⌘C')}{/snippet}{#snippet embedItem()}{@render row('🧩', 'Embed', '⌘B')}{/snippet}
<ContextMenu />Promise-Aware Actions
Section titled “Promise-Aware Actions”When an action’s onclick returns a Promise, the clicked item shows a loading spinner while it is pending (after a short delay, so fast actions never flash one), and the menu stays open until the promise resolves.
View code
<script> import { contextMenu, ContextMenu } from '@delightstack/components';
let status = $state('');
function syncToCloud() { status = 'Syncing…'; return new Promise((resolve) => { setTimeout(() => { status = 'Synced to cloud'; resolve(); }, 1500); }); }</script>
<div {@attach contextMenu({ actions: [ { label: 'Sync to cloud', onclick: syncToCloud }, { label: 'Rename', onclick: () => status = 'Renamed' }, ]})}> Right-click and choose "Sync to cloud"</div>
<ContextMenu />contextMenu() Attachment
Section titled “contextMenu() Attachment”The contextMenu function is a Svelte attachment that registers context menu options for an element. It uses a WeakMap internally for garbage-collectible associations.
<div {@attach contextMenu(options)}> Right-click target</div>ContextMenuOptions
Section titled “ContextMenuOptions”interface ContextMenuOptions { actions: Array<{ onclick?: (event: PointerEvent) => void | Promise<void>; href?: string; target?: '_blank' | '_self' | '_parent' | '_top'; active?: boolean; label?: string; disabled?: boolean; icon?: Component; snippet?: Snippet<[ContextMenuOptions & { el: HTMLElement }]>; }>;}The ContextMenu component itself has no props — it is a global singleton that listens for contextmenu events on the window and renders the appropriate menu via Popover.
interface ContextMenuOptions { actions: Array<{ onclick?: (event: PointerEvent) => void | Promise<void>; href?: string; target?: '_blank' | '_self' | '_parent' | '_top'; active?: boolean; label?: string; disabled?: boolean; icon?: Component; snippet?: Snippet<[ContextMenuOptions & { el: HTMLElement }]>; }>;}Accessibility
Section titled “Accessibility”- Menu items are rendered using
ListandListItemcomponents with proper semantics - Keyboard activation on menu items via Enter/Space
- Escape closes the context menu
- The menu closes on scroll to prevent stale positioning
- Context tracking uses a
WeakMapfor automatic cleanup when elements are removed from the DOM