State & lifecycle

Control open state and the active index with bindings, createHandle(), or both, and know when callbacks fire. Part of Lightbox.

Controlled state

Bind open and index on Root to drive the lightbox from URL params, a store, or sibling UI. Leave them unbound and Root keeps its own state, starting closed at index 0.

The React version splits this into open/onOpenChange, value/onValueChange, and defaultOpen/defaultValue. In Svelte, bind: covers all three: pass a plain value for the initial state, bind it to control it.

open = false
Gallery.svelte
<script lang="ts">	import { Lightbox } from '@ramka/svelte';	let open = $state(false);	let index = $state(0);</script><Lightbox.Root bind:open bind:index>	<!-- triggers + content --></Lightbox.Root>

Imperative API

Lightbox.createHandle() lets Triggers and Root live apart: pass the same handle to both. Call open(index, { triggerId }) from any button so the open still morphs from the matching Trigger. The handle also has close(), goTo(index), and a reactive isOpen.

Product photos

Gallery.svelte
<script lang="ts">	import { Lightbox } from '@ramka/svelte';	let { images }: { images: { id: string; src: string; alt: string }[] } = $props();	const lightbox = Lightbox.createHandle();</script><button type="button" onclick={() => lightbox.open(0, { triggerId: 'photo-0' })}	>View gallery</button>{#each images as img, i (img.id)}	<Lightbox.Trigger handle={lightbox} id="photo-{i}" index={i}>		<img src={img.src} alt={img.alt} />	</Lightbox.Trigger>{/each}<Lightbox.Root handle={lightbox}>	<Lightbox.Portal>		<Lightbox.Backdrop />		<Lightbox.Content>			<Lightbox.Slides>				{#each images as img, i (img.id)}					<Lightbox.Slide>						<Lightbox.Item index={i}>							<Lightbox.Zoom>								<Lightbox.Media>									<img src={img.src} alt={img.alt} />								</Lightbox.Media>							</Lightbox.Zoom>						</Lightbox.Item>					</Lightbox.Slide>				{/each}			</Lightbox.Slides>			<Lightbox.Close aria-label="Close" />		</Lightbox.Content>	</Lightbox.Portal></Lightbox.Root>

Inside one component you can skip the handle: bind:this on Root exposes show(index) and close().

Lifecycle callbacks

onOpenChange fires immediately when open flips: use it to sync the URL or external state. onOpenChangeComplete fires after enter and exit motion settles: use it for cleanup and focus. onValueChange reports the new index plus a NavigationSource (including imperative for handle navigation).

Event log

Open or close the lightbox to see events.

Gallery.svelte
<script lang="ts">	import { Lightbox } from '@ramka/svelte';	let { children } = $props();</script><Lightbox.Root	onOpenChange={(open) => console.log('sync URL or store', open)}	onOpenChangeComplete={(open) => console.log('motion settled, clean up or move focus', open)}	onValueChange={(index, { source }) => console.log(index, source)}>	{@render children()}</Lightbox.Root>

onBeforeOpen

When View Transitions are on, open is prepare → optional image decode → optional onBeforeOpen → morph. Use Root’s onBeforeOpen to delay the morph until custom media is ready (iframe load, video, WebGL). It runs after the lightbox is mounted hidden, so destination nodes exist, and before startViewTransition.

Keep awaitImageDecode for photos (default true). For non-image destinations, pass awaitImageDecode={false} and wait in onBeforeOpen. Bound your own timeout; ctx.signal aborts if a newer open or close wins. See 3D models.

onBeforeClose

onBeforeClose fires synchronously when close begins and, with View Transitions, before the old snapshot is captured. Use it to freeze custom media (video.pause(), stop a rAF loop) so the close morph isn’t a mid-frame. Unlike onBeforeOpen, it cannot delay close. onOpenChange(false) is too late for this: it runs inside the transition callback, after the snapshot.