Popover & ContextMenu
Two surfaces built from the same three attachments: float puts
them where they fit, click_outside takes them away, and focus_trap owns the keyboard while they are up.
Popover
The trigger snippet receives the attributes to spread onto whatever you want to open
it with — the click handler and the aria-expanded/aria-haspopup/aria-controls wiring. Dismissal happens on the press, so a right-click anywhere else closes it too,
and Escape closes the innermost surface only.
svelte<script lang="ts">
import { Popover } from '$lib'
import type { Placement } from '$lib/utils'
let placement = $state<Placement>(`bottom`)
let last_close = $state(``)
</script>
<label>
Placement
<select bind:value={placement}>
{#each [`top`, `right`, `bottom`, `left`] as side (side)}<option>{side}</option
>{/each}
</select>
</label>
<div style="display: flex; gap: 1em; margin: 2em 0; align-items: center">
<Popover {placement} on_close={({ via }) => (last_close = via)}>
{#snippet trigger(props)}
<button {...props}>Open popover</button>
{/snippet}
<p style="margin: 0 0 6pt">Tab is trapped in here.</p>
<label>Name <input placeholder="type something" /></label>
</Popover>
<Popover placement="right" align="start">
{#snippet trigger(props)}
<button {...props}>Second popover</button>
{/snippet}
<p style="margin: 0">Escape closes whichever one you opened last.</p>
</Popover>
{#if last_close}<small>last closed via <code>{last_close}</code></small>{/if}
</div>escape={false} and trap_focus={false} opt out of those behaviors, strategy="absolute" survives a transformed ancestor at the cost of tracking page
scroll, and match_width sizes the surface to the trigger for dropdown-like menus.
Set role to menu, listbox, tree or grid when appropriate; the trigger mirrors it in aria-haspopup and only sets aria-controls while the surface is mounted.
Use trigger_mode="hover" or trigger_mode="focus" for non-click interactions. Hover mode also opens on focus, and open_delay/close_delay keep the surface stable while the pointer crosses from its trigger. Non-click modes default to a 150 ms close delay.
svelte<script lang="ts">
import { Popover } from '$lib'
</script>
<Popover trigger_mode="hover" open_delay={150} close_delay={250} trap_focus={false}>
{#snippet trigger(props)}
<button {...props}>Hover or focus</button>
{/snippet}
<a href="https://svelte.dev">Interactive content stays open while focused.</a>
</Popover>ContextMenu
Right-click a region to replace the browser’s own menu. Actions are CmdActions,
the same shape CommandMenu takes, so a command can appear in both.
Arrow keys walk the items (skipping disabled ones and wrapping at both ends), Home and End jump to either end, Tab stays inside, and Escape or a press anywhere else closes.
Drop the region and the whole page qualifies, trigger="none" neither — bind at and
open the menu yourself from a long-press, a keyboard shortcut or a trigger that must
record what was clicked. dismiss merges over the default { escape: true } to reach
the whole click_outside config, and an item snippet
renders rows your own way.