Gallery✨ Premium

Zoom Lightbox

The photo grows straight out of the clicked thumbnail to the centre of the screen, then goes back into it on closing.

JSZoomTransform

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

Photos pinned along a hiking route — Zoom Lightbox example 1

① Photos pinned along a hiking route

WhenIn the Ostravelle trail log, four photos sit on the map where they were taken. A click on the one at the pass makes it grow out of its spot on the route.
WhyHere the thumbnail's position is information: seeing it open from the pass links the photo to the place. Opening in the centre, with no origin, would lose that link.
SettingsThe map is the root's background (background-image); .zlb-grid is set to display: block and each thumbnail is placed with position: absolute (74 px, 3 px white border). Capture: click on the thumbnail at the 6th mouse move, close with ✕ at the 13th.
Upholstery fabric swatches opened from their slot — Zoom Lightbox example 2

② Upholstery fabric swatches opened from their slot

WhenOn the Melizanne workshop page, the customer browses six fabrics and clicks “Sand dots” to see the pattern up close before ordering a sample square.
WhyIn a swatch book you compare neighbouring slots; seeing the large image leave its slot and return to it helps you keep track of which one you are looking at.
SettingsThree-column grid with name and material under each thumbnail, --zlb-backdrop: rgba(40, 32, 24, 0.9) for a brown background.
Photo received in a chat, enlarged from the bubble — Zoom Lightbox example 3

③ Photo received in a chat, enlarged from the bubble

WhenInès sends a beach photo; you tap the preview in the bubble, it grows to full screen, and a tap beside it sends it back into the bubble.
WhyThat is the gesture people expect in a messaging app: the image seems to come out of the thread and go back, so you never lose your place in the conversation.
SettingsA single thumbnail as wide as the bubble (170 px, --zlb-radius: 11px). The root sits inside the bubble itself.

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; its alt becomes 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: reduce there 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.

Chrome 88+✓ Full (measured)
Firefox 98+✓ Full (measured)
Safari 15.4+✓ Full (measured on WebKit)
Edge 88+✓ Full (Chromium engine)
Mobile iOS 15.4+✓ Full (not measured on a device)
Android Chrome 88+✓ Full (touch measured)

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):

index.html — structure
<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 / propertyDefaultEffect
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

The thumbnail is cropped to 3:2 while your photo may be taller or wider: the animation goes from one rectangle to the other and stretches the image a little for 400 ms. Give .zlb-thumb the same format as your photos for a perfectly proportional zoom.
On closing, the thumbnail's position is measured again: the image returns to wherever the thumbnail is at that moment, even if it has moved.
They would draw the same path, but they only exist since Chrome 111, Safari 18 and Firefox 144, and need a unique transition name per image. The FLIP calculation with element.animate() works wherever the <dialog> works, with no setup.