CSSImage Gallery Layouts

Image Gallery Layouts

A photo gallery is a small collision of several CSS problems at once: a responsive grid that reflows without media queries, images that must not distort regardless of their source aspect ratio, and — if you want a lightbox — a way to show one image full-size without leaving the page. None of it needs JavaScript-heavy libraries anymore.

The responsive grid

repeat(auto-fit, minmax(...)) (see repeat(), auto-fill & auto-fit) is the standard gallery grid: it fits as many columns as the container allows, and each column never shrinks below the minmax floor.

HTML
<div class="gallery">
  <img src="a.jpg" alt="Description of photo A" />
  <img src="b.jpg" alt="Description of photo B" />
  <img src="c.jpg" alt="Description of photo C" />
  <!-- ... -->
</div>

CSS
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.75rem;
}

.gallery img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 8px;
  display: block;
}
  • aspect-ratio: 1 / 1 forces every tile to the same square shape regardless of the source image dimensions — no layout jump while images load.

  • object-fit: cover (see object-fit) crops to fill that box instead of distorting or letterboxing.

  • minmax(220px, 1fr) sets the smallest a tile can go before wrapping to fewer columns — tune it for your thumbnail size.

Tip
Reserve space with aspect-ratio or explicit width/height attributes on the <img> itself — both prevent cumulative layout shift while images are still downloading.
Masonry-style with CSS columns

True masonry (items packed by shortest column, not fixed rows) has an experimental grid-template-rows: masonry in some browsers, but the reliably cross-browser approximation is multi-column layout (see Multi-Column Layout), which lets each image keep its natural aspect ratio.

CSS
.masonry {
  columns: 4 220px; /* up to 4 columns, each at least 220px */
  column-gap: 0.75rem;
}

.masonry img {
  width: 100%;
  display: block;
  margin-bottom: 0.75rem;
  border-radius: 8px;
  break-inside: avoid; /* don't split an image across columns */
}
Note
Columns fill top-to-bottom, left-to-right, so the reading order in the DOM does not match the visual left-to-right order — fine for a purely decorative gallery, worth avoiding if image order carries meaning (a numbered step-by-step sequence, for example).
Hover overlays

CSS
.tile {
  position: relative;
  overflow: hidden;
  border-radius: 8px;
}

.tile .caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 0.75rem;
  background: linear-gradient(to top, rgb(0 0 0 / 0.7), transparent);
  color: white;
  transform: translateY(100%);
  transition: transform 0.2s ease;
}

.tile:hover .caption,
.tile:focus-within .caption {
  transform: translateY(0);
}
Note
Pair :hover with :focus-within (see :focus-visible & :focus-within) so keyboard users tabbing to the link inside a tile see the same caption a mouse user gets on hover.
Lazy loading

HTML
<img
  src="thumb.jpg"
  alt="Sunset over the harbor"
  loading="lazy"
  decoding="async"
  width="400"
  height="400"
/>
  • loading="lazy" is a native HTML attribute (not CSS) but belongs in every gallery — the browser defers off-screen image requests until they near the viewport.

  • decoding="async" lets the browser decode the image off the main thread instead of blocking rendering.

  • Always pair lazy loading with explicit width/height (or aspect-ratio in CSS) so the browser can reserve space before the image arrives.

A CSS-only lightbox with <dialog>

For a full-size preview, wrap each thumbnail in a button that opens a <dialog> containing the large image (see Modal & Dialog Patterns for the full trapping/animation story).

HTML
<button class="tile" onclick="document.getElementById('lb-1').showModal()">
  <img src="thumb.jpg" alt="Sunset over the harbor" />
</button>

<dialog id="lb-1" class="lightbox">
  <img src="full.jpg" alt="Sunset over the harbor" />
  <form method="dialog"><button aria-label="Close">✕</button></form>
</dialog>

CSS
.lightbox {
  border: none;
  padding: 0;
  background: transparent;
  max-width: 90vw;
  max-height: 90vh;
}

.lightbox img {
  display: block;
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
}

.lightbox::backdrop {
  background: rgb(0 0 0 / 0.8);
}
Note
<dialog> gives you Escape-to-close and focus trapping automatically, so the "lightbox" needs no JavaScript beyond the one line that calls showModal().
Grid vs columns vs flex for galleries

Approach

Best for

Grid + auto-fit/minmax

Uniform tiles, e-commerce thumbnails, predictable row alignment

Multi-column (columns)

Pinterest-style masonry where natural image height is part of the design

Flexbox + flex-wrap

Variable-width tiles (e.g. differing aspect ratios you want to keep) packed left to right