Context Menu
Menus & Navigationimport { ContextMenu } from "bits-ui" Context actions
Right-click the target surface to open actions at the pointer
Deployment card
Right-click anywhere on this surface
No action yet
<script lang="ts">
import { ContextMenu } from 'bits-ui';
import { Copy, MousePointer2, Pencil, Trash2 } from '@lucide/svelte';
let lastAction = $state('No action yet');
const itemClass =
'flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50 data-disabled:opacity-40';
</script>
<div class="grid justify-items-center gap-3">
<ContextMenu.Root>
<ContextMenu.Trigger class="card flex min-h-48 w-full max-w-md select-none items-center justify-center border-dashed">
<div class="text-center">
<MousePointer2 class="mx-auto mb-3 size-7 text-brand-400" />
<p class="text-sm font-medium text-ink-50">Deployment card</p>
<p class="muted mt-1 text-xs">Right-click anywhere on this surface</p>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content class="panel animate-pop-in w-60 p-1">
<ContextMenu.Item class={itemClass} onSelect={() => (lastAction = 'Editing deployment')}>
<Pencil class="size-4 text-ink-400" />
Edit deployment
<kbd class="ml-auto font-mono text-[0.68rem] text-ink-400">⌘ E</kbd>
</ContextMenu.Item>
<ContextMenu.Item class={itemClass} onSelect={() => (lastAction = 'Deployment duplicated')}>
<Copy class="size-4 text-ink-400" />
Duplicate
<kbd class="ml-auto font-mono text-[0.68rem] text-ink-400">⌘ D</kbd>
</ContextMenu.Item>
<ContextMenu.Separator class="my-1 h-px bg-ink-800" />
<ContextMenu.Item class={`${itemClass} text-accent-500`} onSelect={() => (lastAction = 'Delete requested')}>
<Trash2 class="size-4" />
Delete deployment
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
<p class="muted text-xs" aria-live="polite">{lastAction}</p>
</div>
View preferences
Checkbox state and a nested preview-size submenu stay inside the contextual workflow
Right-click the canvas for view options
<script lang="ts">
import { ContextMenu } from 'bits-ui';
import { Check, ChevronRight, Eye, Grid2X2 } from '@lucide/svelte';
let showGrid = $state(true);
const itemClass =
'flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50';
</script>
<ContextMenu.Root>
<ContextMenu.Trigger class="card mx-auto grid min-h-48 w-full max-w-md place-items-center border-dashed p-5">
<div class="w-full">
<div class="grid grid-cols-3 gap-2 opacity-75">
{#each Array(6) as _, index (index)}
<div class="h-10 rounded-md border border-ink-800 bg-ink-900"></div>
{/each}
</div>
<p class="muted mt-4 text-center text-xs">Right-click the canvas for view options</p>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content class="panel animate-pop-in w-60 p-1">
<ContextMenu.CheckboxItem bind:checked={showGrid} class={itemClass}>
{#snippet children({ checked })}
<Grid2X2 class="size-4 text-ink-400" />
Show layout grid
{#if checked}<Check class="ml-auto size-4 text-brand-400" />{/if}
{/snippet}
</ContextMenu.CheckboxItem>
<ContextMenu.Sub>
<ContextMenu.SubTrigger class={itemClass}>
<Eye class="size-4 text-ink-400" />
Preview at
<ChevronRight class="ml-auto size-4 text-ink-400" />
</ContextMenu.SubTrigger>
<ContextMenu.SubContent class="panel animate-pop-in w-44 p-1" sideOffset={8}>
<ContextMenu.Item class={itemClass}>Mobile · 390 px</ContextMenu.Item>
<ContextMenu.Item class={itemClass}>Tablet · 768 px</ContextMenu.Item>
<ContextMenu.Item class={itemClass}>Desktop · 1440 px</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Separator class="my-1 h-px bg-ink-800" />
<ContextMenu.Item class={itemClass}>Reset canvas zoom</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu.Root>
The Trigger owns the right-click surface rather than a separate button. Once open, arrow keys move the highlighted item, Enter or Space selects, and Escape dismisses; disabled items are skipped. Item, CheckboxItem, RadioItem, and SubTrigger all accept textValue for typeahead when their rendered label is not plain text.
Content is portalled to document.body by default and uses the pointer location as its anchor. CheckboxItem exposes bindable checked and indeterminate states; its children snippet receives the resolved checked state for rendering an indicator.