3 Overlays
L4stIdi0t edited this page 2026-10-06 12:08:41 +02:00

Overlays

Dialog, CommandPalette, Menu, ContextMenu, Popover, Tooltip, Select, MultiSelect, Combobox, the list of TagsInput and Toaster share the same plumbing, and it is worth knowing what it guarantees.

They render into <body>. A position: fixed element is positioned against the nearest ancestor with a transform, filter, backdrop-filter, contain or will-change — not against the viewport. Without a portal, a dialog opened from inside an animated card renders inside the card, and no z-index rescues it. Because they are portalled, that cannot happen, and an overflow: hidden or scrolling ancestor cannot clip a dropdown either.

They are placed, not positioned. An anchored surface flips to the other side of its trigger when the preferred side does not fit, clamps itself inside the viewport, and re-places while the page scrolls or resizes. A select at the bottom of the window opens upwards.

A modal makes the rest of the page inert. Dialog traps Tab, sets inert on everything else — so a screen reader's virtual cursor cannot wander behind it — locks body scroll with the scrollbar compensated so the layout does not jump, and returns focus to whatever opened it. The Toaster is deliberately exempt: a toast raised over a modal stays announced and dismissable.

Escape and focus behave. Escape closes; focus returns to the opener for a menu, a popover and a dialog, and stays on the trigger for a select, because that is where the APG pattern puts it.

If you build your own floating surface, the same helpers are exported:

<script lang="ts">
	import { portal, track } from '@l4st/driftwood';

	let anchor: HTMLElement;
	let surface: HTMLElement;

	$effect(() => {
		if (!open) return;
		return track(anchor, surface, { side: 'bottom', align: 'start', gap: 6 });
	});
</script>