A gallery shows one large picture, with thumbnails to choose from. This one draws its pictures with CSS gradients, so it runs anywhere, and the same code works with real image URLs.
<style>
.gallery { font-family: system-ui, sans-serif; max-width: 360px; }
.stage { height: 160px; border-radius: 10px; display: grid; place-items: center; color: #fff; font-size: 22px; font-weight: 700; }
.controls { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; }
.controls button { font: inherit; padding: 6px 12px; cursor: pointer; }
.thumbs { display: flex; gap: 6px; }
.thumbs button { flex: 1; height: 40px; border: 3px solid transparent; border-radius: 6px; cursor: pointer; }
.thumbs button[aria-current="true"] { border-color: #d94a00; }
.thumbs button:focus-visible { outline: 3px solid #1a5276; }
</style>
<div class="gallery" id="gallery" tabindex="0" aria-label="Image gallery. Use the left and right arrow keys.">
<div class="stage" id="stage" role="img"></div>
<div class="controls">
<button id="prev">Previous</button>
<span id="caption" role="status"></span>
<button id="next">Next</button>
</div>
<div class="thumbs" id="thumbs"></div>
</div>
<script>
const images = [
{ label: "Sunrise", background: "linear-gradient(135deg, #f5b041, #d94a00)" },
{ label: "Ocean", background: "linear-gradient(135deg, #1a5276, #85c1e9)" },
{ label: "Forest", background: "linear-gradient(135deg, #1e8449, #a9dfbf)" },
{ label: "Night", background: "linear-gradient(135deg, #1c1b1f, #6c3483)" }
];
let index = 0;
const stage = document.querySelector("#stage");
const caption = document.querySelector("#caption");
const thumbs = document.querySelector("#thumbs");
// build the thumbnails from the data
images.forEach((image, i) => {
const button = document.createElement("button");
button.style.background = image.background;
button.setAttribute("aria-label", `Show ${image.label}`);
button.dataset.index = i;
thumbs.append(button);
});
function show(newIndex) {
index = (newIndex + images.length) % images.length; // wrap around at both ends
const image = images[index];
stage.style.background = image.background;
stage.textContent = image.label;
stage.setAttribute("aria-label", `${image.label}, picture ${index + 1} of ${images.length}`);
caption.textContent = `${index + 1} / ${images.length}`;
thumbs.querySelectorAll("button").forEach((button, i) => {
button.setAttribute("aria-current", String(i === index));
});
}
document.querySelector("#prev").addEventListener("click", () => show(index - 1));
document.querySelector("#next").addEventListener("click", () => show(index + 1));
thumbs.addEventListener("click", event => {
const button = event.target.closest("button[data-index]");
if (button) show(Number(button.dataset.index));
});
document.querySelector("#gallery").addEventListener("keydown", event => {
if (event.key === "ArrowLeft") show(index - 1);
if (event.key === "ArrowRight") show(index + 1);
});
show(0);
</script>
The ideas used
- Data first: the
imagesarray drives everything: thumbnails, the main view, the counter. - One function,
show(), updates all the parts.(newIndex + length) % lengthwraps around at both ends. - Event delegation on the thumbnails,
dataset.indexto know which was clicked. - Accessibility: real buttons, labels for thumbnails,
aria-currentfor the active one, a status caption, arrow keys.
Using real images
Replace the gradient with <img> elements. Always give each image meaningful alt text:
const images = [
{ src: "photos/lake.jpg", alt: "A calm mountain lake at sunrise" },
{ src: "photos/forest.jpg", alt: "A path through a green forest" }
];
mainImage.src = images[index].src;
mainImage.alt = images[index].alt;
Try changing it
- Preload the next image to avoid a flicker.
- Add a Slideshow button with
setInterval, and pause it when the user hovers, focuses or presses it (and never auto-start for people who prefer reduced motion). - Open the large image in a modal.
- Support swipe with pointer events.