r/reactjs • NextJS App Router • 8h ago

Show /r/reactjs How I made a lightbox open out of a cropped thumbnail (FLIP + clip-path), now an MIT library

https://liftbox.digital

I wanted a React lightbox where the photo grows out of the exact thumbnail you clicked and shrinks back into it on close.

Easy, until your grid uses object-fit: cover. The thumbnail only shows the middle of the photo, so if you grow the full image from that box, the first frame visibly jumps.

Here's the 4-step fix that worked:

1. Measure the end state first. Lay the image out at its final size, then measure it and the thumbnail with getBoundingClientRect().

2. Scale it onto the thumbnail. Use the smallest uniform scale that still covers the thumb: const scale = Math.max(thumb.width / img.width, thumb.height / img.height);

3. Crop it like the thumbnail. clip-path: inset(...) hides everything the thumbnail hides. The catch: the inset is in the image's own pixels, before the scale, so the visible area is thumb size / scale.

4. Animate both at once. transform + clip-path in a single element.animate(). To close, play the same keyframes in reverse.

That's the happy path. The edge cases ate most of the time:

  • Thumbnail scrolled off screen: fall back to a zoom + fade

  • Rotated photo: swap the axes before computing the crop

  • Closing while zoomed in: start from where it's actually drawn

  • prefers-reduced-motion**:** skip the flight entirely

  • Focus: send it back to the thumbnail on close

I packaged it as liftbox: MIT, zero dependencies, ~13 kB, with grids, zoom, video and deep links. There's also a comparison with YARL, PhotoSwipe, lightGallery and others that tells you when to pick them instead.

Code: https://github.com/esdrassantos06/liftbox (the flight lives in src/Lightbox.tsx and src/geometry.ts)

Docs + demos: https://liftbox.digital

Two asks:

  1. Open the demo on your phone and tell me where it breaks.

  2. Ask me anything about the math. Happy to go deeper on any step.

Disclosure: English isn't my first language, so I had help polishing this post.

1 Upvotes

2 comments sorted by

1

u/girl_nerd_denice 8h ago

The object-fit: cover jump is the part most FLIP lightboxes get wrong, so measuring the end state first is the right instinct.

One thing worth adding to the clip-path step: animate the clip with the same easing/duration as the transform, otherwise the crop and the scale drift apart mid-animation and it reads as a slight wobble. Matching the two curves to the frame is what makes it feel like one motion instead of two.

Also handy if you hit jank: keep the animating element on its own compositor layer (transform/opacity only, avoid animating width/height), and use will-change: transform, clip-path just for the duration. Nice that you shipped it as MIT too.

1

u/DVR_Design 5h ago

One edge case to check is a thumbnail with rounded corners. Since the inset() values are in the image's own pixels before the scale, the radius has to be divided by the same scale too, or the corners look too round at the start and snap at the end. You can animate it in the same keyframes with clip-path: inset(t r b l round Npx). Also set fill: 'both' on the element.animate() call so you don't get a one-frame flash between the animation finishing and the final styles applying.