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
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
modelValue | boolean | boolean | false | Whether the lightbox overlay is open. |
items | array | Array<unknown> | [] | Gallery items. Image items usually provide src, thumbnail, alt, title, width, and height. |
index | number | number | 0 | Active gallery item index. |
origin | object | string | function | Record<string, unknown> | — | Element, Vue ref, selector, DOMRect, or resolver used as the thumbnail origin. |
label | string | string | 'Image preview' | Accessible label for the overlay. |
showThumbnails | boolean | boolean | true | Show a thumbnail navigation strip when multiple items are available. |
loop | boolean | boolean | true | Wrap previous/next navigation at the ends of the gallery. |
closeOnBackdrop | boolean | boolean | true | Close when the dimmed backdrop is clicked. |
duration | number | number | 420 | Thumbnail flyout transition duration in milliseconds. |
Auto-generated from Lightbox.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #(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
| Name | Payload | Description |
|---|---|---|
| @update:modelValue | ( | Emitted when the lightbox requests an open-state change. |
| @update:index | ( | Emitted when navigation changes the active item. |
| @close | — | Emitted when the close transition begins. |
| @after-enter | — | Emitted when the thumbnail-to-stage transition completes. |
| @after-leave | — | Emitted 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.