Building a Responsive Image Grid for Modern Websites

By · Updated

A developer-first pattern for fast, accessible, CLS-safe image grids using CSS Grid (with a lightweight Flexbox fallback), modern media formats, and build-time automation.

Why Grid First (and How to Avoid CLS)

CSS Grid gives you reliable, gap-controlled layouts that adapt to any viewport without JS. The secret to a smooth experience is reserving space so the page doesn’t jump while images load: use the width and height HTML attributes (or aspect-ratio) and set object-fit: cover for crops.

  • Performance: next-gen formats (avif/webp), srcset/sizes, lazy loading.
  • Accessibility: descriptive alt; captions in <figure>/<figcaption>.
  • Maintainability: a single utility-first stylesheet or Tailwind classes; optional container queries.

Semantic Markup Scaffold

Start with semantic containers so screen readers and search engines understand the gallery. Each item is a <figure> with a caption (optional).

<section aria-labelledby="gallery-title" class="gallery">
  <h2 id="gallery-title" class="sr-only">Project Gallery</h2>

  <figure class="gallery__item">
    <picture>
      <source srcset="/img/hero-1200.avif 1200w, /img/hero-800.avif 800w" type="image/avif">
      <source srcset="/img/hero-1200.webp 1200w, /img/hero-800.webp 800w" type="image/webp">
      <img
        src="/img/hero-800.jpg"
        srcset="/img/hero-800.jpg 800w, /img/hero-1200.jpg 1200w"
        sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
        width="1200" height="800"
        alt="Redesign of a marketing homepage with bold headline"
        loading="lazy" decoding="async" class="gallery__img">
    </picture>
    <figcaption class="gallery__caption">Homepage redesign</figcaption>
  </figure>

  <!-- Repeat items... -->
</section>

The explicit width/height preserves aspect ratio and prevents CLS. sizes tells the browser the expected rendered width for optimal source selection.

Option A: Pure CSS Grid (Auto-Fit Columns)

This pattern creates fluid columns that snap as space allows. It’s minimal, fast, and works in all modern browsers.

/* gallery.css */
.gallery{
  --gap: 1rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--gap);
  align-items: start;
}
.gallery__item{
  display: grid; /* allows caption to size below image */
  gap: .5rem;
}
.gallery__img{
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;          /* safety if width/height omitted */
  object-fit: cover;            /* crops nicely in varying cells */
  border-radius: .5rem;
  background: #f4f6f8;          /* placeholder color */
}
.gallery__caption{
  font-size: .875rem;
  color: #6b7280;               /* gray-500 */
}

Tweak minmax(220px, 1fr) to control the smallest card size; increase to reduce column count on small screens.

Option B: Flexbox Rows (Simple Fallback)

Flexbox won’t align rows like Grid, but it’s a fine fallback if you need broad compatibility or want a quick drops-in-anywhere solution.

.gallery{
  --gap: 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
}
.gallery__item{ width: calc(50% - var(--gap)/2); }
@media (min-width: 640px){ .gallery__item{ width: calc(33.333% - var(--gap)*2/3); } }
@media (min-width: 1024px){ .gallery__item{ width: calc(25% - var(--gap)*3/4); } }

Tailwind-First Variant (One-Liners)

If you’re using Tailwind, the same layout is one class away:

<section aria-labelledby="gallery-title"
  class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
  <figure class="space-y-2">
    <img class="w-full aspect-[3/2] object-cover rounded-xl bg-gray-100"
         src="/img/hero-800.jpg" width="1200" height="800"
         alt="Homepage redesign" loading="lazy" decoding="async">
    <figcaption class="text-sm text-gray-500">Homepage redesign</figcaption>
  </figure>
  <!-- ... -->
</section>

Turn Tiles Into Linkable Cards (Keyboard & Screen Reader Safe)

Wrap the media and caption in a single focusable link. Use a visually subtle, high-contrast focus style.

<a href="/case-studies/homepage/"
   class="group block focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#d4a856] rounded-lg">
  <figure class="space-y-2">
    <img ... class="w-full aspect-[3/2] object-cover rounded-lg group-hover:opacity-95 transition">
    <figcaption class="text-sm text-gray-600">Homepage redesign</figcaption>
  </figure>
</a>

Performance: Formats, Srcset, and Byte Budgets

  • Formats: avifwebpjpg/png fallback in <picture>.
  • Srcset/sizes: ship multiple widths; use realistic sizes so the browser picks correctly.
  • Lazy load: loading="lazy" (but eager-load above-the-fold hero).
  • Preload criticals: for the first visible image when it’s LCP.
  • Automation: generate derivatives at build time (sharp/Squoosh) and enforce a byte budget (e.g., ≤ 150KB per tile).
// scripts/make-images.js (Node + sharp)
import sharp from "sharp"; import fg from "fast-glob"; import { dirname } from "node:path"; import { mkdirSync } from "node:fs";
for (const src of await fg("src/images/*.{jpg,jpeg,png}")){
  const base = src.replace(/^src\/images\//,"").replace(/\.(jpe?g|png)$/i,"");
  const sizes = [640, 800, 1200];
  for (const w of sizes){
    const out = `dist/images/${base}-${w}.webp`;
    mkdirSync(dirname(out), { recursive: true });
    await sharp(src).resize({ width: w }).webp({ quality: 78 }).toFile(out);
  }
}

Optional: Container Queries for Smarter Grids

If the gallery lives inside a narrow column on some pages and full-width on others, container queries adapt columns based on parent width—not the viewport.

.gallery-wrap{ container-type: inline-size; }
@container (min-width: 40rem){
  .gallery{ grid-template-columns: repeat(3, 1fr); }
}
@container (min-width: 64rem){
  .gallery{ grid-template-columns: repeat(4, 1fr); }
}

“Masonry” Looks Without Heavy JS (Progressive)

True CSS masonry support varies, so stick to progressive enhancement. The column-based approach below creates a Pinterest-style flow with minimal code:

.masonry{ column-count: 1; column-gap: 1rem; }
@media (min-width: 640px){ .masonry{ column-count: 2; } }
@media (min-width: 1024px){ .masonry{ column-count: 3; } }
.masonry figure{ break-inside: avoid; margin: 0 0 1rem; }

Use Grid for most galleries; switch to columns when you need the staggered aesthetic and can accept uneven row bottoms.

QA Checklist (Ship With Confidence)

  • ✅ No layout shifts: images have width/height or aspect-ratio.
  • srcset/sizes pick correct source at each breakpoint.
  • ✅ Alt text describes the image; captions are used when context adds value.
  • ✅ Keyboard: tab order reaches links/cards; focus ring visible; lightbox closes on Esc.
  • ✅ Byte budget respected; thumbnails ≤ 150KB; LCP stays ≤ 2.5s on mobile.

FAQs

Should I use aspect-ratio or width/height attributes?

Use width/height whenever you know the intrinsic size—it’s the most robust CLS fix. aspect-ratio is a solid fallback for unknown dimensions.

Is AVIF worth it over WebP?

AVIF typically compresses smaller at similar quality, especially for photos. Provide both via <picture> for broad coverage.

How many breakpoints do I need in srcset?

Three to four well-chosen widths (e.g., 640, 800, 1200, 1600) plus an accurate sizes string beats ten random sizes.

Key Takeaways

  • Grid first, Flexbox as a simple fallback; reserve space to eliminate CLS.
  • Use <picture> with avif/webp, accurate srcset/sizes, and lazy loading.
  • Container queries make galleries adapt to their parent, not just the viewport.
  • Keep it accessible: alt text, captions, focus styles, and an escape-able lightbox.
Disclaimer: This tutorial is provided for educational and informational purposes only and does not constitute legal, financial, or professional advice. All content is offered “as-is” without warranties of any kind. Readers are solely responsible for implementation and must ensure compliance with applicable laws, licensing, and platform terms. Always apply the information only within authorized, ethical, and legal contexts.

Spot an error or a better angle? Tell me and I’ll update the piece. I’ll credit you by name—or keep it anonymous if you prefer. Accuracy > ego.

Portrait of Mason Goulding

Mason Goulding · Founder, Maelstrom Web Services

Builder of fast, hand-coded static sites with SEO baked in. Stack: Eleventy · Vanilla JS · Netlify · Figma

With 10 years of writing expertise and currently pursuing advanced studies in computer science and mathematics, Mason blends human behavior insights with technical execution. His Master’s research at CSU–Sacramento examined how COVID-19 shaped social interactions in academic spaces — see his thesis on Relational Interactions in Digital Spaces During the COVID-19 Pandemic . He applies his unique background and skills to create successful builds for California SMBs.

Every build follows Google’s E-E-A-T standards: scalable, accessible, and future-proof.