The product-page gallery — big image, thumbnail rail underneath — is e-commerce's oldest component and still routinely janky: the page jumps as images load, the rail loses sync with the main image, swiping fights the buttons. All three faults have one-line fixes.
Velocity X's image-carousel-thumbnails is built as two scroll strips sharing one index — the browser does the swiping, React only keeps score.
The Money Pattern
// Main strip: native scroll + snap; an observer reports which slide is current
<div bind:ref={strip} class="flex snap-x snap-mandatory overflow-x-auto">
{images.map((img, i) => (
<figure class="w-full shrink-0 snap-center aspect-[4/3]">
<img src={img.src} alt={img.alt}
loading={i === 0 ? 'eager' : 'lazy'}
class="h-full w-full object-cover" />
</figure>
))}
</div>
// Sync: observer sets index; thumbnails scroll the strip; both stay honest
const goTo = (i: number) =>
strip.current?.children[i]?.scrollIntoView({ behavior: 'smooth', inline: 'center' });
useEffect(() => {
rail.current?.children[index]?.scrollIntoView({ inline: 'nearest', block: 'nearest' });
}, [index]);
The key inversion: thumbnails don't set state — they scroll the strip, and the IntersectionObserver sets state from what's actually visible. One source of truth (the scroll position itself), so swipe, thumbnail clicks and arrow keys can never disagree about which image is current.
The Catch
aspect-ratio on the figure, not the image, is the CLS fix — the gallery owns its height before a single byte of image arrives. Pair it with loading="lazy" on everything except the first slide, and preload slide n+1 when n becomes active so swiping never hits a blank.
The rail needs its own overflow behaviour: inline: 'nearest' keeps the active thumbnail in view without yanking the rail around on every change. And thumbnails are buttons with aria-label="View image 3 of 7" — alt text on the main image, intent on the controls.
The Verdict
Let scroll-snap own the gesture, let the observer own the index, let aspect-ratio own the layout — the gallery becomes 80 lines and no physics library. It's in Velocity X's Carousels & Tickers category, next to the device-frame and testimonial variants built on the same chassis.









