Skip to content

Components

Image Carousels With Thumbnails — Two Strips, One Index, Zero Jank

scroll-snap Owns the Main Strip, scrollIntoView Keeps the Rail Honest, aspect-ratio Kills the Jump
Written by Aiden Wood Founder and Lead Design Engineer
🖼️ 🎞️ 🔁

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.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.