Plenty of image galleries need nothing more than a list of images and a stylesheet. Grid, Flexbox or multi-column layout arranges the photos in a grid, in columns or in a masonry-style pattern, and a script is optional.

Portfolio pages, blogs and product listings use the pattern to resize thumbnails across screen widths.

Even a lightbox can run on stylesheet rules alone. MDN Web Docs lists the :target pseudo-class behind it as available across browsers since July 2015. A script takes over at focus management and swipe gestures, and also when you want to filter by category.

What Counts as a CSS Gallery?

Mostly a plain list of figures. Each one holds an img and, if you want it, a figcaption. That markup gives the stylesheet everything it needs.

Layout, spacing, cropping and hover states all come from CSS rules, which keeps the page light and the gallery easy to restyle later.

Grid, column and masonry-style layouts are fair game for CSS, along with gap, sizing, image cropping, hover zoom and caption overlays. Filtering and sorting by category are not. Neither is focus handling inside a lightbox, and swipe gestures or infinite loading will push you toward JavaScript too.

The term has a second meaning. CSS gallery sites are showcase collections of well-designed websites, and this article does not cover them.

Which Layout Method Fits a CSS Gallery?

CSS Grid suits uniform thumbnails. Multi-column layout copes better with photos of mixed proportions, and native masonry is an extra for the few browsers that support it.

Flexbox sits in between. It handles a single row or a wrapping row well, though the last line needs attention.

MethodCore propertiesBest forMain limitation
CSS Gridgrid-template-columns, gapUniform thumbnails in equal rowsRows share one height, so mixed ratios get cropped
Flexboxflex-wrap, flex-basisOne row or a short wrapping rowLast row stretches when items use flex-grow
Multi-columncolumn-count, column-gapPhotos with different heightsItems run top to bottom inside each column
Native masonrydisplay: grid-lanesLane-based layout without a scriptUnflagged only in Safari (26.4 and later) so far

If every image has the same ratio, use Grid and move on. Mixed ratios where cropping is not acceptable call for multi-column, or grid-lanes with a fallback. For a single strip of thumbnails, Flexbox is enough.

WebKit’s own guidance for browsers without Grid Lanes is to load a JavaScript masonry library as a polyfill, or to fall back to another CSS layout such as multi-column. You can also write Grid Lanes with a CSS fallback, for example a display: grid line declared before display: grid-lanes.

Is responsive design still a top priority?

Explore the latest responsive design statistics: adoption rates, performance impact, user behavior, and trends shaping modern websites.

See the Numbers →

How to Build a Responsive CSS Gallery with Grid

Start with the markup and finish by testing at narrow widths. Everything in between is a few lines of CSS.

  1. Write a ul with one li per image, each holding an img with alt text plus width and height attributes.
  2. Set display: grid on the list, then add grid-template-columns with repeat(auto-fit, minmax(...)) and a gap.
  3. Give each image width: 100% and display: block so it fills its tile.
  4. Add @media rules only where the design calls for fixed column counts.
  5. Resize the browser to 320px and check for horizontal scroll.

Fluid columns with auto-fit

One declaration does most of the responsive work.

.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
list-style: none;
padding: 0;
}

The grid fits as many 16rem columns as the container allows, then lets them grow into the leftover space.

If you would rather see the tracks before typing them, a CSS grid generator outputs the same declaration.

MDN’s repeat() documentation says auto-fit collapses empty tracks after placement. Put 3 images in a wide container and auto-fit stretches the tiles to fill it, while auto-fill keeps the empty tracks around and the tiles stay narrow.

One thing that bites people. A bare minmax(16rem, 1fr) overflows when the container is narrower than 16rem, and the min(100%, 16rem) wrapper is what removes that overflow.

Fixed columns with media queries

Fixed counts beat auto-fit when the design needs exactly four, three, two or one column, because auto-fit can land on five columns at an in-between width.

Write mobile-first media queries that raise the count as the viewport widens. These are starting values, so adjust them to the images.

Viewport widthColumns
1024px and wider4
768px to 1023px3
480px to 767px2
Under 480px1
.gallery { grid-template-columns: 1fr; }

@media (min-width: 480px) {
.gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

The minmax(0, 1fr) form stops a wide image from pushing its track past an equal share.

How Do aspect-ratio and object-fit Keep Gallery Thumbnails Uniform?

The aspect-ratio property fixes the tile shape, and object-fit decides how each photo fills that shape.

.gallery img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}

Pick the ratio from the photos you actually have. Square tiles (1 / 1) suit portfolio and product grids. 4 / 3 is a safe default for landscape-heavy sets, and 16 / 9 gives you wide tiles for video stills.

With object-fit: cover, the image fills the box and keeps its proportions, and the overflow is cropped. contain shows the whole photo and leaves empty bars around it.

Cropping removes edges. Faces or text near the border get cut, and object-position (for example center 20%) moves the crop window toward the subject.

A fixed aspect ratio on the tile also reserves its height before any file arrives, so the grid stops jumping while images load.

The web-features project lists aspect-ratio as Baseline widely available since March 2024, with support from Chrome 88, Firefox 89 and Safari 15.

MDN notes the ratio only takes effect when width or height is auto. If both are fixed, the ratio is ignored, which is an easy one to miss when a tile refuses to change shape.

How to Build a Masonry-Style Gallery in CSS

Multi-column layout builds masonry-style columns in every current browser. display: grid-lanes does the same natively in Safari 26.4 and later, while Chrome, Edge and Firefox keep it behind a flag.

Multi-column masonry

.masonry {
column-count: 3;
column-gap: 1rem;
}

.masonry figure {
break-inside: avoid;
margin: 0 0 1rem;
}

Set column-count or column-width on the container. Add break-inside: avoid to each figure, the same property MDN’s fragmentation guide applies to keep a caption attached to its image.

Items fill column one first, then column two. A viewer scanning left to right across rows sees the sequence out of order.

Ready-made CSS masonry layouts show further variations on this method.

Native grid-lanes and its support status

Safari 26.4 shipped Grid Lanes in March 2026, the first stable browser release with native masonry, according to the WebKit release notes. Safari 27, released in September 2026, keeps it.

.masonry {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
gap: 1rem;
}

Grid Lanes reuses Grid’s track sizing and explicit placement. WebKit also added flow-tolerance, a setting for how readily an item moves to another lane to sit closest to the start.

The syntax was disputed for a while. The Chrome team argued in its own post that masonry belongs in a separate display: masonry mode, while WebKit built it into Grid. WebKit’s January 2026 status post says the key syntax decisions are final and Chromium engineers at Edge are updating their implementation.

Chrome and Edge 140 and later carry an earlier experiment behind a flag, and as of October 2026 it is still disabled by default in Chrome’s stable channel. Firefox keeps only its older grid-template-rows: masonry prototype behind a flag. Apple’s WWDC26 session “Learn CSS Grid Lanes” describes Grid Lanes as available now in Safari 26.4.

Layer it over the column layout with a feature query.

@supports (display: grid-lanes) {
.masonry {
display: grid-lanes;
column-count: auto;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
gap: 1rem;
}
.masonry figure { margin: 0; }
}

Visitors on other browsers get the multi-column version. Nothing breaks, but the column ordering limit above still applies to them.

How to Add Hover Effects and Captions to a CSS Gallery

Wrap each image in a figure with overflow: hidden, scale the img on hover, and slide a figcaption up from the bottom edge.

.gallery figure {
position: relative;
overflow: hidden;
margin: 0;
}

.gallery img {
display: block;
width: 100%;
transition: transform 0.3s ease;
}

.gallery figcaption {
position: absolute;
inset: auto 0 0 0;
padding: 0.75rem;
color: #fff;
background: linear-gradient(transparent, rgb(0 0 0 / 0.75));
transform: translateY(100%);
transition: transform 0.3s ease;
}

.gallery figure:hover img,
.gallery figure:focus-within img {
transform: scale(1.06);
}

.gallery figure:hover figcaption,
.gallery figure:focus-within figcaption {
transform: translateY(0);
}

The overflow: hidden on the figure keeps the zoomed photo inside its tile. Without it, the scaled image spills over its neighbors.

Zoom is the safe default. For tilt, blur or color shifts, browse these CSS hover effects and keep the durations short.

A 0.3s ease feels immediate, while past 0.5s the effect lags behind the pointer. A CSS transition generator previews timing curves before you commit to one.

Overlay captions save vertical space and suit photo-heavy sets, but the text sits on a variable background. A caption below the image is always readable and needs no hover logic, at the price of one extra line per tile.

Touch screens have no hover, so a caption that only appears on :hover never shows. The :focus-within selectors above cover keyboard users when the image sits inside a link, and a hover query covers touch.

@media (hover: none) {
.gallery figcaption { transform: none; }
}

The prefers-reduced-motion media feature has worked in all major browsers since January 2020 and reached Baseline widely available status in July 2022, according to the web-features project.

@media (prefers-reduced-motion: reduce) {
.gallery img,
.gallery figcaption { transition: none; }

.gallery figure:hover img,
.gallery figure:focus-within img { transform: none; }
}

Can You Build a Lightbox with Only CSS?

Yes, for small galleries. The :target pseudo-class opens a full-size image from a URL fragment, and a radio-button variant adds previous and next controls. Neither manages focus, the Escape key or swipe.

With the :target method, each thumbnail links to #photo-1 and the matching overlay appears when its id matches the fragment (the pattern in MDN’s :target reference). The radio or checkbox hack does it differently, since hidden inputs hold the open state while labels act as thumbnails and as next or previous buttons.

<a href="#photo-1"><img src="thumb-1.jpg" alt="Harbor at dawn"></a>

<div class="lightbox" id="photo-1">
<a href="#" class="close">Close</a>
<img src="full-1.jpg" alt="Harbor at dawn">
</div>
.lightbox {
display: none;
position: fixed;
inset: 0;
place-items: center;
background: rgb(0 0 0 / 0.9);
}

.lightbox:target { display: grid; }

There is no focus trap, so Tab moves through the page behind the open overlay, and there is no Escape key close either. Each open adds a history entry, which means Back closes the lightbox instead of leaving the page.

Fades are missing by default because display flips instantly. Getting one needs opacity and visibility in a transition, or transition-behavior: allow-discrete with @starting-style, which became Baseline in 2024.

The SLaks CSS-Gallery project builds a filterable gallery with a lightbox and previous and next links entirely in CSS and LESS. Its README still advises against building real interactive applications without JavaScript.

Use the CSS-only lightbox for 6 to 12 images on a simple page. Past that, or anywhere keyboard access matters, hand the lightbox to a script.

How Do Lazy Loading, srcset and Image Formats Affect Gallery Performance?

Lazy loading cuts the bytes a gallery requests up front, srcset and sizes stop small tiles from downloading large files, and WebP or AVIF shrink each file. The first visible row still has to load eagerly.

Google’s PageSpeed Insights thresholds call an LCP of 2.5 s or less good at the 75th percentile, and a CLS of 0.1 or less counts as good too (same thresholds).

On pages that use lazy loading, the median 75th percentile LCP was 3,546 ms, against 2,922 ms on pages that do not (web.dev). Lossy WebP files come out 25% to 34% smaller than JPEG at an equal SSIM index (Google WebP compression study).

Lazy loading belongs below the fold

The web.dev gap above is a correlation, but its follow-up WordPress experiment pointed the same way. Lazy loading images already inside the viewport hurt LCP, and limiting it to below-the-fold images reversed the regression.

Put loading="lazy" on every tile after the first row. A hero image above the gallery or the first row usually becomes the LCP element, so keep it eager.

Mark the single likely LCP image with fetchpriority="high". WordPress core adds that attribute to a single image per response, and Lighthouse flags a lazy-loaded LCP image, so run it before launch.

srcset, sizes and modern formats

<picture>
<source type="image/avif"
srcset="photo-400.avif 400w, photo-800.avif 800w"
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 480px) 50vw, 100vw">
<source type="image/webp"
srcset="photo-400.webp 400w, photo-800.webp 800w"
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 480px) 50vw, 100vw">
<img src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w"
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 480px) 50vw, 100vw"
width="800" height="600" loading="lazy" alt="Harbor at dawn">
</picture>

The sizes values mirror the column counts of the grid: 25vw, 33vw, 50vw, then 100vw. A sizes string that overstates the tile width makes the browser download files larger than it needs.

The picture element lets the browser pick the first format it supports and fall back to JPEG. AVIF usually compresses photographs further than WebP but encodes more slowly, so convert at build time instead of on request.

How to Make a CSS Gallery Accessible

Useful alt text, visible keyboard focus, a DOM order that matches the intended reading path and controls large enough to hit cover most of it.

RequirementGallery elementFix
Text alternativeEach imgAlt text that describes the content
Focus visibilityLinks wrapping thumbnailsA visible :focus-visible outline
Focus Not Obscured (Minimum) (2.4.11, AA)Tile focused under a sticky headerscroll-padding-top equal to the header height
Target Size (Minimum) (2.5.8, AA)Lightbox close and arrow buttonsAt least 24 by 24 CSS pixels

W3C published WCAG 2.2 as a Recommendation on 5 October 2023 with 9 new success criteria, and 2 of them hit galleries directly. Target Size (Minimum) requires pointer targets of at least 24 by 24 CSS pixels unless spacing or another listed exception applies.

For alt text, describe what the photo shows. Use alt="" when the image is decorative. And don’t paste the caption word for word into the alt attribute, because screen readers read both.

.gallery a:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 3px;
}

Tab order follows the DOM, not the layout. Grid placement and the order property change what people see without changing where Tab goes, so reorder in the markup instead.

A script-driven lightbox needs role="dialog", aria-modal="true" and a label, but ARIA attributes only describe the widget. The script still has to move focus into the dialog and return it to the thumbnail on close.

Caption text over photos fails color contrast checks wherever the image is light. Put a dark gradient or a solid band behind the text and test against the lightest photo in the set.

When Does a CSS Gallery Stop Being Enough?

When the page needs filtering, sorting, swipe, infinite loading or a lightbox with focus management. Those behaviors need state and events that a stylesheet cannot hold.

Adding JavaScript for one of those jobs does not mean abandoning the layout. The grid or columns stay in CSS, and the script handles only the missing behavior.

LibrarySolvesLicense terms
PhotoSwipeTouch lightbox with swipe and zoomMIT (npm listing)
FancyboxLightbox for images, video and moreGPLv3, commercial license for commercial applications (fancyBox 3 README)
IsotopeFiltering and sortingGPLv3 for open source, commercial license for commercial projects (Metafizzy license page)

Lightbox2 covers the simple case through a data-lightbox attribute on each image link. Masonry by David DeSandro places items in row order, which multi-column cannot, so it earns its place only when that order matters.

Staying with CSS alone means no script to download, parse or maintain, and the layout renders before any JavaScript loads. The cost is no focus trapping, no Escape handling and no swipe, and filtering or sorting needs generated selectors for every combination.

Adding a library brings touch gestures, zoom and filtering out of the box. In return you get an extra download and a dependency to update, license terms on commercial sites (Isotope, Fancybox), and keyboard and screen reader behavior that depends on the library, so test it.

My preference is to keep the layout in CSS and add one narrow script for the missing behavior. PhotoSwipe fits a touch lightbox, and Isotope belongs in the build only when category filtering is a real requirement.

A full gallery plugin brings its own markup and styles that fight the grid you already wrote. Past roughly a dozen images, or at the first filtering requirement, the script earns its weight.

CSS Gallery Examples To Check Out

Travel Gallery

Rhomb Gallery on Grids + Clip-path

CSS 3D Transform Gallery

Art Gallery

Amur Leopard Image Gallery With CSS VARS

Quad Image Gallery

Fashion Isometric Layout

Reflective Vibes with Shawn’s Gallery Wall

3D Randomness by Josetxu

Let Your Pics Shine with This Responsive Delight

Magnific’s Marvel

Joe’s Off-Canvas Genius

Expanding Horizons with Afif

Slide into Teegan’s World

Pinterest’s Lil’ Sis: Tumblr Photogrid

Balkan’s Fresh Beats

Spin Me Round 3D Gallery

Wavy Baby Gallery

Peek-A-Boo! Before & After Snapshots

Step Into the Virtual Reality Realm

Pup Parade with CSS Marquee

Floaty Feels with Parallax Gallery

Pizza Slices or Grid Design?

Tilt It, Scroll It, Love It

KaBuKi’s 3D Mastery

Flip Out with this Unique Gallery

Ettrics’ Google-y Delight

Splitting Views, Telling Tales

Jordan’s Snapshot Spectacle

A Gallery of Furry Love

CSS Gallery FAQ

How do I build a slider with CSS scroll snap?

Set display: flex, overflow-x: auto and scroll-snap-type: x mandatory on the container, then add scroll-snap-align: start to each slide.

Swipe and trackpad scrolling work immediately. Arrow buttons and dots still need anchor links or a small script.

Can I filter a gallery without JavaScript?

Yes, for a handful of categories. Hidden radio inputs hold the active filter, and sibling or :has() selectors hide the tiles that do not match.

Every extra category adds selectors, which is why larger filtered galleries move to a script such as Isotope.

How do I add a CSS gallery in WordPress?

Insert a Gallery block, set the column count in its settings, and add a custom class under Advanced. Then write your grid rules for that class in Additional CSS.

The block outputs ordinary figure and img markup, so the layout techniques above apply to it.

How do I center the last row of a flexbox gallery?

Add justify-content: center to the flex container and give the items a fixed flex-basis with no flex-grow.

Items on the last row keep their width and sit in the middle instead of stretching. Full rows look the same as before.

How do I make a hexagon or 3D CSS gallery?

Hexagon tiles use clip-path: polygon() on each image, with six coordinate pairs defining the shape. A CSS clip-path generator outputs those coordinates visually.

D effects add perspective to the container and transform: rotateY() to the tiles, which makes CSS perspective the property to learn first.

Where can I find CSS gallery code examples to copy?

CodePen hosts live, forkable gallery demos, and MDN Web Docs carries tested snippets for single techniques such as the :target lightbox and repeat().

Check each demo’s license before pasting it into a commercial project, and swap the placeholder images for your own sized files.

What a New CSS Gallery Should Ship First

Launch with a grid layout, lazy-loaded thumbnails below the first row and a visible focus outline. Masonry or a script can wait until those pass testing.

Start with the grid and a fixed tile ratio. Then sort out image delivery and first-row loading, and finish with a keyboard pass at 320px in Safari and one Chromium browser.

Structure goes first because image weight and focus behavior both depend on tile size. Enhancements come last. Each one adds weight or a new way to break.

The trade-off is polish: a CSS-only lightbox gives up swipe and focus trapping in exchange for zero script. This advice dates from October 2026 and changes when Chrome and Edge ship grid-lanes without a flag, the point where the multi-column fallback can go.

Galleries that need a sliding strip of featured images lead naturally to CSS carousels.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)