4 Pure CSS3 with JSPublished 2026-10-05Free · no sign-up
Seven gallery layouts: a responsive grid with captions, a lightbox dialog with previous and next, tiles with hover captions, a product gallery with a thumbnail strip, a horizontal snap row, a filterable gallery and a mosaic with tiles of different sizes.
A gallery of figures with captions in an auto-filling grid, every thumbnail cropped to the same 4:3 frame with object-fit: cover. Captions are figcaptions, and images have descriptive alt text that does not just repeat the caption.
View code
<ul class="ig1"><li><figure><div class="im" style="--g:linear-gradient(135deg,#fde68a,#ea580c)" role="img" aria-label="Orange sunset over rooftops"></div><figcaption>Lisbon, July</figcaption></figure></li><li><figure><div class="im" style="--g:linear-gradient(135deg,#bae6fd,#1d4ed8)" role="img" aria-label="Blue lake surrounded by pines"></div><figcaption>Lake Bled</figcaption></figure></li><li><figure><div class="im" style="--g:linear-gradient(135deg,#bbf7d0,#15803d)" role="img" aria-label="Green rice terraces in mist"></div><figcaption>Bali</figcaption></figure></li></ul>
Thumbnails open a lightbox dialog showing the large image with previous and next buttons, a counter and a close button. showModal() traps focus and handles Escape, arrow keys move between images, and focus returns to the thumbnail when it closes.
var thumbs = document.querySelectorAll('.ig2 .th button'), d = document.querySelector('.ig2 dialog'), big = d.querySelector('.big'), cap = d.querySelector('.cap'), i = 0;
function show(k) { i = (k + thumbs.length) % thumbs.length; var t = thumbs[i], name = t.getAttribute('aria-label').replace(/^Open /, ''); big.style.background = t.style.getPropertyValue('--g'); big.setAttribute('aria-label', name); cap.textContent = (i + 1) + ' of ' + thumbs.length + ' · ' + name.replace(/^photo \d+: /, ''); }
thumbs.forEach(function (t, k) { t.onclick = function () { show(k); d.showModal(); }; });
d.querySelector('.pv').onclick = function () { show(i - 1); }; d.querySelector('.nx').onclick = function () { show(i + 1); };
d.querySelector('.x').onclick = function () { d.close(); };
d.addEventListener('keydown', function (e) { if (e.key === 'ArrowRight') show(i + 1); if (e.key === 'ArrowLeft') show(i - 1); });
d.addEventListener('close', function () { thumbs[i].focus(); });
03. Gallery with Hover Captions
GalleryHover
Images zoom slightly inside their frames on hover and a caption slides up over the bottom, for portfolio galleries. Each tile is a link, so the same effect appears on keyboard focus, and captions are always visible on touch screens.
A main image with a vertical strip of thumbnails beside it, the product page gallery. Thumbnails are buttons with aria-pressed, the main image's accessible name changes with the selection, and the strip turns horizontal under it on narrow containers.
var main = document.querySelector('.ig4 .main'), bs = document.querySelectorAll('.ig4 .strip button');
bs.forEach(function (b) { b.onclick = function () { bs.forEach(function (x) { x.setAttribute('aria-pressed', x === b); }); main.style.setProperty('--g', b.style.getPropertyValue('--g')); main.setAttribute('aria-label', 'Linen shirt, ' + b.getAttribute('aria-label').toLowerCase()); }; });
05. Horizontal Snap Gallery
GalleryScroll snap
A single row of images that scrolls sideways with scroll-snap, good for stories and related photos where space is tight. The row is a labelled region that can be focused and scrolled with the arrow keys.
View code
<div class="ig5" tabindex="0" role="region" aria-label="Photos from the workshop, scroll sideways"><figure><div style="--g:linear-gradient(135deg,#fecaca,#b91c1c)"></div><figcaption>Day 1</figcaption></figure><figure><div style="--g:linear-gradient(135deg,#fed7aa,#c2410c)"></div><figcaption>Day 2</figcaption></figure><figure><div style="--g:linear-gradient(135deg,#fef08a,#a16207)"></div><figcaption>Day 3</figcaption></figure><figure><div style="--g:linear-gradient(135deg,#bbf7d0,#15803d)"></div><figcaption>Day 4</figcaption></figure></div>
Category buttons filter a photo grid, with the active filter marked by aria-pressed and a live count of photos shown. Hidden photos use the hidden attribute, so they also leave the accessibility tree and the tab order.
var bs = document.querySelectorAll('.ig6 button'), lis = document.querySelectorAll('.ig6 li'), p = document.querySelector('.ig6 p');
bs.forEach(function (b) { b.onclick = function () { var c = b.dataset.c, k = 0; bs.forEach(function (x) { x.setAttribute('aria-pressed', x === b); }); lis.forEach(function (li) { li.hidden = c !== 'all' && li.dataset.c !== c; if (!li.hidden) k++; }); p.textContent = k + ' photos'; }; });
07. Mosaic Gallery with Spans
GalleryMosaic
A mosaic where some photos span two columns or two rows, set with a couple of classes on a regular grid. It breaks the monotony of equal tiles while keeping the images in a predictable reading order.
Every image needs alt text that describes what matters in it. A caption and alt text should complement each other rather than repeat word for word.
Same frame, any photo
Crop thumbnails to one aspect ratio with object-fit: cover so the grid lines up whatever shape the originals are.
Lightboxes are dialogs
Open large images in a <dialog> with showModal(): focus is trapped, Escape closes it and focus returns to the thumbnail. Add arrow keys for previous and next.
Lazy-load the rest
Give real images width, height and loading="lazy" so galleries do not slow the first load or shift the layout.
Frequently asked questions
Where are the photos?
The demos use gradients as stand-ins. Swap in img elements with alt text.
Do I need a lightbox library?
No. The dialog element covers focus, Escape and the backdrop.
Masonry or grid for a gallery?
A grid is tidier and keeps order; see the masonry collection for mixed heights.