Popover
Overlaysimport { Popover } from "bits-ui" Anchored form
Interactive content anchored below its trigger with a visible positioning arrow
<script lang="ts">
import { Popover } from 'bits-ui';
import { Settings2, X } from '@lucide/svelte';
let width = $state(1280);
let height = $state(720);
</script>
<Popover.Root>
<Popover.Trigger class="btn btn-primary">
<Settings2 class="size-4" />
Canvas size
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side="bottom"
align="center"
sideOffset={10}
class="panel animate-pop-in z-[100] w-72 p-4 shadow-2xl outline-none"
>
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="text-sm font-semibold text-ink-50">Canvas dimensions</h3>
<p class="mt-1 text-xs text-ink-400">Set the exported frame size in pixels.</p>
</div>
<Popover.Close class="btn btn-ghost btn-sm size-7 p-0" aria-label="Close popover">
<X class="size-3.5" />
</Popover.Close>
</div>
<div class="mt-4 grid grid-cols-2 gap-3">
<label class="text-xs font-medium text-ink-200" for="popover-width">
Width
<input id="popover-width" class="input mt-1 w-full" type="number" min="320" bind:value={width} />
</label>
<label class="text-xs font-medium text-ink-200" for="popover-height">
Height
<input id="popover-height" class="input mt-1 w-full" type="number" min="240" bind:value={height} />
</label>
</div>
<p class="mt-3 font-mono text-xs text-ink-400">{width} × {height}</p>
<Popover.Arrow class="fill-ink-800" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
Side and alignment
The same floating panel uses explicit side, align, and sideOffset combinations
<script lang="ts">
import { Popover } from 'bits-ui';
const placements = [
{ label: 'Start', side: 'bottom' as const, align: 'start' as const, offset: 6 },
{ label: 'Top', side: 'top' as const, align: 'center' as const, offset: 12 },
{ label: 'End', side: 'bottom' as const, align: 'end' as const, offset: 18 }
];
</script>
<div class="flex flex-wrap items-center justify-center gap-8 py-8">
{#each placements as placement}
<Popover.Root>
<Popover.Trigger class="btn btn-ghost">{placement.label}</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side={placement.side}
align={placement.align}
sideOffset={placement.offset}
class="panel animate-pop-in z-[100] w-52 p-4 shadow-2xl outline-none"
>
<p class="text-sm font-medium text-ink-50">{placement.side} · {placement.align}</p>
<p class="mt-1 text-xs leading-5 text-ink-400">sideOffset: {placement.offset}px</p>
<Popover.Arrow class="fill-ink-800" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
{/each}
</div>
Popover.Portal moves floating content to document.body by default; Content still measures and follows its trigger through Floating UI. The resolved
placement appears in data-side and data-align, while --bits-popover-content-transform-origin supports placement-aware animation.
Popover content traps focus by default, focuses its first focusable control on open, and restores
focus to the trigger on close. Escape and outside pointer-down both close it by default; configure escapeKeydownBehavior, interactOutsideBehavior, or their cancellable event
callbacks on Popover.Content to change that. Unlike dialogs, popovers do not lock body
scrolling or block outside interaction unless preventScroll is enabled.