SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/image-galleries

7 CSS Image Galleries

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.

01. Responsive Gallery with Captions

GalleryGrid

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>
.ig1 { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 12px; width: 480px; max-width: 100%; margin: 0; padding: 0; list-style: none; font: 13px Inter, system-ui, sans-serif; }
.ig1 figure { margin: 0; } .ig1 .im { aspect-ratio: 4 / 3; border-radius: 10px; background: var(--g); }
.ig1 figcaption { margin-top: 6px; color: #475467; }

02. Lightbox with Previous and Next

GalleryDialog

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.

View code
<div class="ig2"><div class="th"><button style="--g:linear-gradient(135deg,#f472b6,#7c3aed)" aria-label="Open photo 1: Orchid"></button><button style="--g:linear-gradient(135deg,#34d399,#0e7490)" aria-label="Open photo 2: Fern"></button><button style="--g:linear-gradient(135deg,#fbbf24,#c2410c)" aria-label="Open photo 3: Marigold"></button></div><dialog aria-label="Photo viewer"><div class="big" role="img"></div><p class="cap" aria-live="polite"></p><div class="ctl"><button class="pv" aria-label="Previous photo">&larr;</button><button class="x">Close</button><button class="nx" aria-label="Next photo">&rarr;</button></div></dialog></div>
.ig2 .th { display: flex; gap: 8px; }
.ig2 .th button { width: 80px; height: 80px; border: 0; border-radius: 10px; background: var(--g); cursor: pointer; }
.ig2 dialog { width: min(320px, 92vw); padding: 12px; border: 0; border-radius: 16px; background: #0f172a; color: #e2e8f0; font: 14px Inter, system-ui, sans-serif; }
.ig2 dialog::backdrop { background: rgba(0,0,0,.75); }
.ig2 .big { aspect-ratio: 4 / 3; border-radius: 10px; }
.ig2 .cap { margin: 8px 0; text-align: center; }
.ig2 .ctl { display: flex; justify-content: space-between; }
.ig2 .ctl button { padding: 8px 14px; border: 0; border-radius: 8px; background: #1e293b; color: #fff; font: 600 14px Inter, system-ui, sans-serif; cursor: pointer; }
.ig2 button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
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.

View code
<ul class="ig3"><li><a href="#p1"><span class="im" style="--g:linear-gradient(135deg,#a5b4fc,#3730a3)" aria-hidden="true"></span><span class="c">Brand refresh</span></a></li><li><a href="#p2"><span class="im" style="--g:linear-gradient(135deg,#fda4af,#9f1239)" aria-hidden="true"></span><span class="c">Packaging</span></a></li><li><a href="#p3"><span class="im" style="--g:linear-gradient(135deg,#86efac,#166534)" aria-hidden="true"></span><span class="c">Web shop</span></a></li><li><a href="#p4"><span class="im" style="--g:linear-gradient(135deg,#fcd34d,#92400e)" aria-hidden="true"></span><span class="c">Signage</span></a></li></ul>
.ig3 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; width: 380px; max-width: 100%; margin: 0; padding: 0; list-style: none; font: 600 13px Inter, system-ui, sans-serif; }
.ig3 a { position: relative; display: block; aspect-ratio: 1; overflow: hidden; border-radius: 12px; text-decoration: none; }
.ig3 .im { position: absolute; inset: 0; background: var(--g); transition: transform .4s ease; }
.ig3 .c { position: absolute; left: 0; right: 0; bottom: 0; padding: 18px 10px 8px; background: linear-gradient(transparent, rgba(0,0,0,.7)); color: #fff; transform: translateY(100%); transition: transform .3s ease; }
.ig3 a:hover .im, .ig3 a:focus-visible .im { transform: scale(1.06); }
.ig3 a:hover .c, .ig3 a:focus-visible .c { transform: none; }
.ig3 a:focus-visible { outline: 3px solid #528bff; outline-offset: 2px; }
@media (hover: none) { .ig3 .c { transform: none; } }
@media (prefers-reduced-motion: reduce) { .ig3 .im, .ig3 .c { transition: none; } }

04. Large Image with Thumbnail Strip

GalleryProduct

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.

View code
<div class="ig4"><div class="in"><div class="main" role="img" aria-label="Linen shirt, front" style="--g:linear-gradient(160deg,#e7e5e4,#a8a29e)"></div><div class="strip"><button aria-pressed="true" aria-label="Front" style="--g:linear-gradient(160deg,#e7e5e4,#a8a29e)"></button><button aria-pressed="false" aria-label="Back" style="--g:linear-gradient(160deg,#d6d3d1,#78716c)"></button><button aria-pressed="false" aria-label="Detail" style="--g:linear-gradient(160deg,#f5f5f4,#57534e)"></button></div></div></div>
.ig4 { width: 420px; max-width: 100%; container-type: inline-size; }
.ig4 .in { display: grid; gap: 8px; }
.ig4 .main { aspect-ratio: 4 / 5; border-radius: 12px; background: var(--g); }
.ig4 .strip { display: flex; gap: 8px; order: 2; }
.ig4 .strip button { width: 56px; height: 56px; border: 2px solid transparent; border-radius: 8px; background: var(--g); cursor: pointer; }
.ig4 .strip [aria-pressed=true] { border-color: #f8fafc; }
.ig4 button:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
@container (min-width: 360px) { .ig4 .in { grid-template-columns: 60px 1fr; } .ig4 .strip { order: 0; flex-direction: column; } }
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>
.ig5 { display: flex; gap: 10px; width: 320px; max-width: 100%; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 6px; font: 600 12px Inter, system-ui, sans-serif; }
.ig5:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.ig5 figure { flex: 0 0 140px; margin: 0; scroll-snap-align: start; }
.ig5 figure div { aspect-ratio: 3 / 4; border-radius: 10px; background: var(--g); }
.ig5 figcaption { margin-top: 4px; color: #475467; }

06. Filterable Gallery

GalleryFilter

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.

View code
<div class="ig6"><div class="f" role="group" aria-label="Show"><button aria-pressed="true" data-c="all">All</button><button aria-pressed="false" data-c="city">City</button><button aria-pressed="false" data-c="nature">Nature</button></div><p aria-live="polite">6 photos</p><ul><li data-c="city" role="img" aria-label="City photo 1" style="--g:#475569"></li><li data-c="nature" role="img" aria-label="Nature photo 2" style="--g:#15803d"></li><li data-c="city" role="img" aria-label="City photo 3" style="--g:#334155"></li><li data-c="nature" role="img" aria-label="Nature photo 4" style="--g:#0e7490"></li><li data-c="nature" role="img" aria-label="Nature photo 5" style="--g:#4d7c0f"></li><li data-c="city" role="img" aria-label="City photo 6" style="--g:#52525b"></li></ul></div>
.ig6 { width: 320px; max-width: 100%; font: 13px Inter, system-ui, sans-serif; }
.ig6 .f { display: flex; gap: 6px; }
.ig6 button { padding: 6px 12px; border: 1px solid #334155; border-radius: 999px; background: transparent; color: #cbd5e1; font: 600 13px Inter, system-ui, sans-serif; cursor: pointer; }
.ig6 [aria-pressed=true] { background: #f8fafc; color: #0f172a; }
.ig6 button:focus-visible { outline: 2px solid #93c5fd; outline-offset: 2px; }
.ig6 p { margin: 8px 0; color: #94a3b8; }
.ig6 ul { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin: 0; padding: 0; list-style: none; }
.ig6 li { aspect-ratio: 1; border-radius: 8px; background: var(--g); }
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.

View code
<ul class="ig7"><li role="img" aria-label="Photo 1" class="w" style="--g:linear-gradient(135deg,#c7d2fe,#4338ca)"></li><li role="img" aria-label="Photo 2" style="--g:linear-gradient(135deg,#fecdd3,#be123c)"></li><li role="img" aria-label="Photo 3" class="t" style="--g:linear-gradient(135deg,#a7f3d0,#047857)"></li><li role="img" aria-label="Photo 4" style="--g:linear-gradient(135deg,#fde68a,#b45309)"></li><li role="img" aria-label="Photo 5" style="--g:linear-gradient(135deg,#e9d5ff,#7e22ce)"></li></ul>
.ig7 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 80px; gap: 6px; width: 380px; max-width: 100%; margin: 0; padding: 0; list-style: none; }
.ig7 li { border-radius: 10px; background: var(--g); }
.ig7 .w { grid-column: span 2; } .ig7 .t { grid-row: span 2; }

About these CSS Image Galleries

Alt text is the content

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.