Skip to content

ContextMenu

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';

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).

Right-click in this area to open the context menu
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 />

Pass a Svelte component to an action’s icon property to render it before the label.

Right-click for file options
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 />

Actions can navigate using href and target instead of onclick.

Right-click for navigation options
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 />
Right-click to change view (current: grid)
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 />

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.

Right-click to share
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 />

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.

Right-click and choose “Sync to cloud”
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 />

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>
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 }]>;
}>;
}
  • Menu items are rendered using List and ListItem components 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 WeakMap for automatic cleanup when elements are removed from the DOM