webdevpuneet.com

Thumbnail Gallery — Free HTML CSS JS Image Snippet

Thumbnail Gallery · Media · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="tg-gallery" id="tgGallery">
  <div class="tg-main">
    <div class="tg-stage" id="tgStage" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">
      <span class="tg-stage-label" id="tgLabel">Aurora</span>
    </div>
  </div>
  <div class="tg-thumbs" id="tgThumbs" role="tablist" aria-label="Product images"></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Main view plus thumbnail strip
The classic product-gallery layout: one large square image and a clickable row of thumbnails beneath it.
Smooth cross-fade
Selecting a thumbnail fades the main image out and the new one in over 180ms instead of a hard cut.
Aspect-ratio locked
Both the stage and thumbnails use aspect-ratio: 1/1 so the gallery scales fluidly with no fixed heights.
Data-driven
An IMAGES array builds every thumbnail and handler, so adding images means editing one array.
Active-state veil
A CSS ::after overlay dims inactive thumbs, lightens on hover, and clears with a border on the active one — no JS for hover.
Keyboard navigation
Arrow keys move through thumbnails with wrap-around and move focus, matching tablist conventions.
Accessible buttons
Each thumbnail is a real <button> with role=tab and aria-selected so screen readers track the current view.
No assets required
Runs on CSS gradients out of the box; swap to <img> URLs for real product photos.

About this UI Snippet

Thumbnail Gallery — Main Image with Clickable Thumbnail Strip and Cross-Fade

Screenshot of the Thumbnail Gallery snippet rendered live

The thumbnail gallery is the standard way e-commerce sites show multiple product photos: one large main image, a row of smaller thumbnails beneath it, and a click on any thumbnail swaps it into the main view. Amazon, Shopify storefronts, and almost every product detail page use this pattern because it lets a shopper see one image clearly while keeping every alternate angle one click away. This component implements it in plain HTML, CSS, and vanilla JavaScript, with a smooth cross-fade between images, an active-thumbnail highlight, and full keyboard navigation. It uses CSS gradients as stand-in images so it runs with no external assets, but swapping in real <img> tags is a one-line change.

Layout: aspect-ratio main stage and a grid of thumbs

The main view uses aspect-ratio: 1 / 1 so it stays perfectly square at any width without a fixed pixel height — the modern replacement for the old padding-top percentage hack. The thumbnail strip is a five-column CSS grid with a consistent gap, and each thumbnail also uses aspect-ratio: 1 / 1 so the row of thumbs is always uniform regardless of the container width. Because both the stage and thumbs are ratio-locked, the whole gallery scales fluidly inside its max-width wrapper.

Data-driven rendering

The gallery is driven by an IMAGES array of { label, bg } objects. On load, the script loops over the array with document.createElement to build each thumbnail <button>, attach its click handler, and apply its background. This data-driven approach means adding or removing an image is a matter of editing the array — no markup changes. Each thumbnail is a real <button> (not a div) so it is focusable and clickable by keyboard out of the box, and it carries role="tab" and aria-selected so assistive technology understands the gallery as a set of selectable views.

The cross-fade transition

Clicking a thumbnail does not snap the main image — it cross-fades. The select() function adds a .fading class that animates the stage's opacity to 0 over 180ms, then in a setTimeout it swaps the background and label and removes the class, fading the new image back in. This fade-out-swap-fade-in sequence hides the hard cut and makes the gallery feel polished. The timeout duration is matched to the CSS transition so the swap happens exactly at the invisible midpoint. A guard at the top of select() returns early if you click the already-active thumbnail, so there is no pointless fade.

Active-state highlighting

The selected thumbnail gets an .active class that shows an indigo border and removes a translucent white overlay. Every thumbnail has a ::after pseudo-element that lays a semi-transparent white veil over it; inactive thumbs are dimmed by this veil, the hovered thumb is partly cleared, and the active thumb is fully cleared and bordered. This is a pure-CSS way to make the current selection pop without changing the image itself, and it gives instant hover feedback with no JavaScript.

Keyboard navigation

The thumbnail strip listens for ArrowRight and ArrowLeft on keydown. Pressing an arrow computes the next index with wrap-around using (active + dir + length) % length — so the selection cycles from the last thumb back to the first and vice versa — calls select(), and moves focus to the newly active thumbnail with .focus(). This makes the gallery fully operable without a mouse, matching the keyboard expectations of a tablist.

Customisation

To use real photos, replace the gradient bg values with image URLs and render an <img src> inside each thumbnail and the stage instead of setting a background. Change grid-template-columns: repeat(5, 1fr) to show more or fewer thumbnails per row, swap the #6366f1 active-border colour for your brand, and adjust the 180ms fade timing (in both the CSS transition and the JS timeout together) to make swaps faster or slower.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA square main image renders above a row of five thumbnails, with the first thumbnail active.
  2. 2
    Click a thumbnailThe main image cross-fades to the selected image and its label updates; the clicked thumbnail gets the indigo active border.
  3. 3
    Use arrow keysFocus a thumbnail and press the left/right arrows to move through the images with wrap-around, focus following the selection.
  4. 4
    Swap in real imagesReplace the gradient bg values in the IMAGES array with image URLs and render <img> tags in the stage and thumbnails.
  5. 5
    Add or remove imagesEdit the IMAGES array — thumbnails and behaviour update automatically since rendering is data-driven.
  6. 6
    Tune the lookChange the thumbnail column count, the active border colour, and the fade duration (CSS transition + JS timeout) to taste.

Real-world uses

Common Use Cases

Product detail pages
Show every angle of a product with a clean main view — pair it with a variant selector and a quantity stepper to build a full buy box.
Portfolio and case-study galleries
Let visitors page through screenshots or shots of a project; for a full-screen view add an image lightbox on main-image click.
Real-estate and listing photos
Present interior and exterior shots of a property with thumbnails for quick jumping between rooms.
Recipe and tutorial steps
Use thumbnails as step markers and the main view to show the current step's photo.
Color and material previews
Show fabric, finish, or colour options as thumbnails that swap a large preview, complementing a color swatch picker.
Learning gallery patterns
A reference for data-driven rendering, CSS cross-fades, and accessible tablist keyboard navigation.

Got questions?

Frequently Asked Questions

Change each IMAGES entry to { label, src: 'photo.jpg' }. In the build loop, append an <img src={img.src}> to the thumbnail instead of setting fill.style.background, and in select() set the stage to show an <img> (or use background-image: url(...)) rather than a gradient. Add object-fit: cover to the images so they fill their square containers without distortion.

Edit grid-template-columns: repeat(5, 1fr) on .tg-thumbs — change 5 to your desired count. For a scrolling strip instead of a wrapping grid, set .tg-thumbs to display: flex; overflow-x: auto and give each thumb a fixed flex-basis. The cross-fade and keyboard logic are independent of the layout, so neither needs changes.

Yes. Add setInterval(() => select((active + 1) % IMAGES.length), 4000) after the setup. Pause it on hover or focus by clearing the interval in a mouseenter/focusin handler and restarting it on mouseleave/focusout, so the user can study an image without it changing under them.

The opacity transition lives in CSS (0.35s on .tg-stage), but the image swap must happen while the stage is invisible — at the midpoint of the fade. The JS setTimeout of 180ms swaps the background at that point, then removes .fading to fade the new image in. If you change the fade speed, update both the CSS transition and the JS timeout together so the swap stays hidden.

Store the active index in state and render the IMAGES array with .map (React), v-for (Vue), or *ngFor (Angular). The main view's background/src binds to IMAGES[active], and each thumbnail's onClick sets the index. For the cross-fade, toggle a "fading" class via state and use a timeout (cleared on unmount) to swap, or simpler, animate opacity with a CSS transition keyed off the active index. The arrow-key handler attaches to the strip's onKeyDown.