Zoom Lightbox
The photo grows straight out of the clicked thumbnail to the centre of the screen, then goes back into it on closing.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Gallery has 44 effects, including 6 free. Explore the category →
3 usage examples



How it works
The technique is called FLIP. On a click on a .zlb-thumb button, the script opens the <dialog>, waits until the large image knows its size (img.decode()), then measures two rectangles: the thumbnail's and the image's at its final place. From them it derives an offset and a scale, and runs an element.animate() animation that starts on the thumbnail (translate + scale) and ends at transform: none.
The way in lasts 400 ms with the effect's original curve (cubic-bezier(0.175, 0.885, 0.32, 1.275), a small overshoot); the 90 % black background fades in meanwhile. On closing (✕, Escape or a click on the background), the same animation plays backwards, in 400 accelerating ms, towards where the thumbnail is at that moment; the dialog then closes.
Measured in Chromium, Firefox and WebKit: when the animation starts, the image centre matches the thumbnail centre to the pixel. If the thumbnail and the image have different proportions, the image is slightly stretched during those 400 ms, then gets its proportions back.
Accessibility
- Buttons: each thumbnail is a
<button>reachable with Tab and opened with Enter or Space; itsaltbecomes the large image's. - Modal dialog: dialog role and the name “Image viewer”, page behind made inert, focus on ✕ when it opens, Escape to close.
- Focus returned: it lands back on the thumbnail the image came from, right where the image has just tucked itself in.
- Reduced motion: with
prefers-reduced-motion: reducethere is no zoom and no fade; the image appears directly in the centre and vanishes at once. - Touch: a tap opens like a click. This viewer has no navigation between images: one image per opening.
Browser compatibility
element.animate() exists since Chrome 36, Firefox 48 and Safari 13.1, img.decode() since Chrome 64, Firefox 68 and Safari 11.1; the limiting factors remain the modal <dialog> (Firefox 98, Safari 15.4) and aspect-ratio (Chrome 88). Zoom starting point measured in Chromium 151, Firefox 153 and WebKit 26.5, with two galleries on the page.
Without img.decode(), the zoom waits for the image's load event. Without showModal(), nothing opens and the thumbnails stay plain photos.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0173-root">
<div class="zlb-grid">
<button type="button" class="zlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Cyan to emerald gradient" width="600" height="400"></button>
<button type="button" class="zlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Amber to red gradient" width="600" height="400"></button>
<button type="button" class="zlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Indigo to violet gradient" width="600" height="400"></button>
<button type="button" class="zlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Violet to cyan gradient" width="600" height="400"></button>
</div>
<dialog class="zlb-viewer" aria-label="Image viewer">
<div class="zlb-content">
<button type="button" class="zlb-close" aria-label="Close"><svg>…</svg></button>
<img class="zlb-image" alt="">
</div>
</dialog>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Zoom duration (JS — CLOSE_MS) |
400 ms | Used for the way in, the way back and the delay before closing; also adjust the CSS opacity transition. |
Opening curve (JS — easing in zoomBetween) |
cubic-bezier(0.175, 0.885, 0.32, 1.275) |
The last number above 1 gives the small bounce; use ease-out for an arrival without overshoot. |
Thumbnail width (CSS — --zlb-thumb) |
120px | 3:2 format; you can also position each thumbnail freely, the zoom always starts from its real place. |
Background (CSS — --zlb-backdrop) |
rgba(0, 0, 0, 0.9) |
Tint of the overlay that darkens during the zoom. |
Corners (CSS — --zlb-radius, --zlb-radius-lg) |
0.5rem, 1rem | Corners of the thumbnail and of the enlarged image. |
Full-resolution photo (HTML — data-full) |
absent | Loaded on opening; the zoom starts once it is decoded. |
FAQ
.zlb-thumb the same format as your photos for a perfectly proportional zoom.element.animate() works wherever the <dialog> works, with no setup.