Component

Lightbox

<DomLightbox>

A gallery overlay that opens from a thumbnail into a stable preview stage, then flies back to the origin on close.

Demo

Gallery lightbox

A shared lightbox controlled from a thumbnail grid. The image clone flies from the clicked card into a stable stage, then returns to that origin on close.

<script setup>
import { shallowRef, ref } from 'vue';
import { DomLightbox } from '@getdom/studio/vue';

const items = [
	{
		id: 'gallery-planning-wall',
		title: 'Planning wall',
		description: 'A quiet workspace image with enough texture to show the thumbnail-to-stage motion clearly.',
		alt: 'A planning wall with notes, sketches, and daylight across a desk.',
		src: 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1600&h=1067&q=85',
		thumbnail: 'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=640&h=427&q=75',
		width: 1600,
		height: 1067,
	},
	{
		id: 'gallery-product-table',
		title: 'Product table',
		description: 'The preview stage uses the item dimensions, so the enlarged media lands at the same aspect ratio as the source.',
		alt: 'A product planning table with devices and notebooks arranged for review.',
		src: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=1600&h=1000&q=85',
		thumbnail: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=640&h=400&q=75',
		width: 1600,
		height: 1000,
	},
	{
		id: 'gallery-studio-detail',
		title: 'Studio detail',
		description: 'Navigation changes the staged image while the close animation still returns to the originating thumbnail.',
		alt: 'A close detail of design material, color cards, and stationery.',
		src: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=1400&h=1750&q=85',
		thumbnail: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=560&h=700&q=75',
		width: 1400,
		height: 1750,
	},
	{
		id: 'gallery-workshop',
		title: 'Workshop',
		description: 'The lightbox is controlled from outside the grid, so it can later be reused for card, calendar, and kanban detail views.',
		alt: 'A team workshop table with laptops and paper notes.',
		src: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=1600&h=1067&q=85',
		thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?auto=format&fit=crop&w=640&h=427&q=75',
		width: 1600,
		height: 1067,
	},
];

const open = ref(false);
const selectedIndex = ref(0);
const originRect = shallowRef(null);

/**
 * Open the lightbox from the clicked gallery card with a stable origin snapshot.
 *
 * @param {number} index Gallery item index.
 * @param {MouseEvent} event Click event used to capture the thumbnail origin.
 * @returns {void}
 */
function openItem(index, event) {
	selectedIndex.value = index;
	originRect.value = originRectFromEvent(event);
	open.value = true;
}

/**
 * Capture the clicked card bounds before Vue updates the overlay state.
 *
 * @param {MouseEvent} event Click event fired by the card button.
 * @returns {DOMRect|null} Snapshot of the clicked card bounds.
 */
function originRectFromEvent(event) {
	return event.currentTarget?.getBoundingClientRect?.() || null;
}

/**
 * Release the retained origin after the fly-back animation finishes.
 *
 * @returns {void}
 */
function resetOrigin() {
	originRect.value = null;
}
</script>

<template>
	<div class="mx-auto w-full max-w-5xl">
		<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
			<button
				v-for="(item, index) in items"
				:key="item.id"
				type="button"
				class="group overflow-hidden rounded-lg border border-border bg-secondary/30 text-left outline-none transition hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/60 focus-visible:ring-2 focus-visible:ring-ring"
				@click="openItem(index, $event)"
			>
				<img
					:src="item.thumbnail"
					:alt="item.alt"
					class="aspect-[4/3] w-full object-cover transition duration-300 group-hover:scale-[1.03]"
				/>
				<span class="block space-y-1 p-3">
					<span class="block truncate text-sm font-semibold tracking-tight text-canvas-fg">{{ item.title }}</span>
					<span class="block truncate text-xs text-muted-fg">{{ item.description }}</span>
				</span>
			</button>
		</div>
	</div>

	<DomLightbox
		v-model="open"
		v-model:index="selectedIndex"
		:items="items"
		:origin="originRect"
		:duration="520"
		@after-leave="resetOrigin"
	/>
</template>

Design note

Built for media first, reusable for detail views

DomLightbox keeps the visual effect separate from dialog layout. The moving thumbnail is a temporary clone; the centered stage is stable and slot-driven. That gives image galleries the expected lightbox feel now, while leaving a path for kanban cards, calendar events, and form/detail previews to use the same overlay shell later.

Reference

Props

Control props

NameTypeTSDefaultDescription
modelValuebooleanbooleanfalseWhether the lightbox overlay is open.
itemsarrayArray<unknown>[]Gallery items. Image items usually provide src, thumbnail, alt, title, width, and height.
indexnumbernumber0Active gallery item index.
originobject | string | functionRecord<string, unknown>Element, Vue ref, selector, DOMRect, or resolver used as the thumbnail origin.
labelstringstring'Image preview'Accessible label for the overlay.
showThumbnailsbooleanbooleantrueShow a thumbnail navigation strip when multiple items are available.
loopbooleanbooleantrueWrap previous/next navigation at the ends of the gallery.
closeOnBackdropbooleanbooleantrueClose when the dimmed backdrop is clicked.
durationnumbernumber420Thumbnail flyout transition duration in milliseconds.

Auto-generated from Lightbox.props and inline _edit hints.

Slots

NameScopeDescription
#(default){ item, index, close, next, previous, goTo, phase }Optional custom stage content. Omit for the built-in image preview.
#caption{ item, index }Optional caption content below the stage.
#thumbnail{ item, index, active }Optional thumbnail strip rendering.

Events

NamePayloadDescription
@update:modelValue(open
open: boolean;
: boolean)
Emitted when the lightbox requests an open-state change.
@update:index(index
index: number;
: number)
Emitted when navigation changes the active item.
@closeEmitted when the close transition begins.
@after-enterEmitted when the thumbnail-to-stage transition completes.
@after-leaveEmitted when the fly-back transition completes and retained data is released.
@navigate({ index, item, direction })Emitted after arrow or thumbnail navigation.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.

Keyboard

  • EscCloses the lightbox.
  • Left / RightMoves between gallery items.
  • Click backdropCloses the lightbox when closeOnBackdrop is true.