Use Cases (Updated: 6/2/2026)

Build a Fast Image Gallery with Claude Code

Use Claude Code to build a responsive image gallery with React, srcset, lightbox behavior, pitfalls, and checks.

Build a Fast Image Gallery with Claude Code

Building an image gallery with Claude Code is not only a layout exercise. A gallery can sell a product, explain a case study, document a workshop, or help a reader compare before-and-after screenshots. If the prompt only says “make a beautiful masonry gallery,” Claude Code may produce a nice demo while missing the details that matter in production: image weight, alt text, layout shift, keyboard behavior, broken CMS data, and the path from an image to the next action.

This guide rewrites the image gallery task as a practical implementation workflow. We will give Claude Code tighter requirements, build a typed React component, add responsive image attributes, cover concrete use cases, and review the failure modes that usually appear after launch. For related foundations, pair this with Claude Code image processing, performance optimization, and accessibility implementation. Keep the official references nearby: Claude Code docs, MDN on responsive images, MDN on lazy loading, and WCAG 2.2.

My practical rule is to make Claude Code solve the data contract before the visual treatment. Once every image has an id, category, dimensions, and meaningful alternative text, the UI becomes much easier to review. If you use the word harness, explain it for beginners as the agent’s working scaffold: the constraints, files, commands, and checks that keep the agent from wandering across unrelated code.

Prompt Claude Code with Constraints

Use a prompt like this before asking for visual polish. It tells Claude Code what must be preserved, what code must be real, and what review evidence should come back with the diff.

Implement a React image gallery.
The goal is a fast UI for articles, case studies, product screenshots, and workshop photos.

Constraints:
- Do not break existing routing or design system conventions.
- Define an image data type with required id, src, alt, width, height, and category.
- Use CSS Grid for the responsive layout.
- Use srcset, sizes, loading, and fetchPriority deliberately.
- Open a lightbox on click and close it with Escape.
- Handle empty arrays, failed images, long alt text, and mobile width.
- After implementation, explain changed files, test coverage, and remaining risks.

Return copy-paste-ready React/TypeScript and CSS, not pseudocode.

The important part is not the exact wording. The important part is that the request creates a small reviewable patch. Dimensions prevent layout shift. Required alt text prevents the usual “image 1” problem. Asking for risk notes pushes Claude Code into a second mode: not just generation, but implementation review.

Implementation Shape

Before code, make the responsibilities visible. Claude Code tends to produce better files when it can see the path from source images to browser behavior.

flowchart LR
  A["Original images"] --> B["Create size variants"]
  B --> C["GalleryImage array"]
  C --> D["Category filtering"]
  D --> E["CSS Grid cards"]
  E --> F["Lightbox"]
  E --> G["Lighthouse and manual checks"]
DecisionSafe defaultRevisit when
LayoutCSS GridImages have extreme height variation
Lazy loadingOnly below-the-fold imagesFirst visible image is delayed
Image variantsAround 480/960/1440pxLarge displays dominate your analytics
LightboxMinimal accessible behaviorThe gallery drives purchase decisions

Starting without a masonry dependency is intentional. Many galleries do not need true masonry; they need stable cards, fast thumbnails, and a clean preview. Smaller code also gives Claude Code fewer places to introduce unrelated changes.

Copy-Paste React Component

The following component is intentionally framework-light. It works in a Vite app, can be moved into a Next.js client component, and can later be adapted to next/image, Astro image assets, or a CDN image service.

import { useEffect, useMemo, useState } from "react";
import "./image-gallery.css";

export type GalleryImage = {
  id: string;
  src: string;
  alt: string;
  width: number;
  height: number;
  category: string;
  sources?: Array<{ width: number; src: string }>;
};

function buildSrcSet(image: GalleryImage) {
  if (!image.sources?.length) return undefined;

  return [...image.sources]
    .sort((a, b) => a.width - b.width)
    .map((source) => `${source.src} ${source.width}w`)
    .join(", ");
}

export function ImageGallery({ images }: { images: GalleryImage[] }) {
  const [category, setCategory] = useState("all");
  const [activeId, setActiveId] = useState<string | null>(null);
  const [brokenIds, setBrokenIds] = useState<Set<string>>(() => new Set());

  const categories = useMemo(() => {
    return ["all", ...Array.from(new Set(images.map((image) => image.category)))];
  }, [images]);

  const visibleImages = useMemo(() => {
    if (category === "all") return images;
    return images.filter((image) => image.category === category);
  }, [category, images]);

  const activeImage = visibleImages.find((image) => image.id === activeId);

  useEffect(() => {
    if (!activeImage) return;

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setActiveId(null);
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [activeImage]);

  function markBroken(id: string) {
    setBrokenIds((current) => new Set(current).add(id));
  }

  if (images.length === 0) {
    return <p className="gallery-empty">No images are available yet.</p>;
  }

  return (
    <section className="gallery" aria-label="Image gallery">
      <div className="gallery-toolbar" aria-label="Filter images by category">
        {categories.map((item) => (
          <button
            className={item === category ? "is-active" : ""}
            key={item}
            onClick={() => setCategory(item)}
            type="button"
          >
            {item === "all" ? "All" : item}
          </button>
        ))}
      </div>

      <div className="gallery-grid">
        {visibleImages.map((image, index) => {
          const isBroken = brokenIds.has(image.id);

          return (
            <button
              className="gallery-card"
              key={image.id}
              onClick={() => setActiveId(image.id)}
              type="button"
            >
              {isBroken ? (
                <span className="gallery-fallback">Image unavailable</span>
              ) : (
                <img
                  alt={image.alt}
                  width={image.width}
                  height={image.height}
                  src={image.src}
                  srcSet={buildSrcSet(image)}
                  sizes="(min-width: 960px) 33vw, (min-width: 640px) 50vw, 100vw"
                  loading={index < 2 ? "eager" : "lazy"}
                  fetchPriority={index === 0 ? "high" : "auto"}
                  style={{ aspectRatio: `${image.width} / ${image.height}` }}
                  onError={() => markBroken(image.id)}
                />
              )}
              <span>{image.alt}</span>
            </button>
          );
        })}
      </div>

      {activeImage && (
        <div
          className="gallery-lightbox"
          role="dialog"
          aria-modal="true"
          aria-label={activeImage.alt}
          tabIndex={-1}
          onClick={() => setActiveId(null)}
        >
          <button className="gallery-close" onClick={() => setActiveId(null)} type="button">
            Close
          </button>
          <img
            alt={activeImage.alt}
            width={activeImage.width}
            height={activeImage.height}
            src={activeImage.src}
            onClick={(event) => event.stopPropagation()}
          />
        </div>
      )}
    </section>
  );
}
.gallery {
  display: grid;
  gap: 1rem;
}

.gallery-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.gallery-toolbar button,
.gallery-card,
.gallery-close {
  border: 1px solid #d4d4d8;
  background: #ffffff;
  color: #18181b;
  cursor: pointer;
}

.gallery-toolbar button {
  border-radius: 999px;
  padding: 0.45rem 0.8rem;
}

.gallery-toolbar .is-active {
  background: #18181b;
  color: #ffffff;
}

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

.gallery-card {
  display: grid;
  gap: 0.5rem;
  padding: 0;
  overflow: hidden;
  border-radius: 8px;
  text-align: left;
}

.gallery-card img {
  width: 100%;
  object-fit: cover;
  background: #f4f4f5;
}

.gallery-fallback {
  display: grid;
  min-height: 180px;
  place-items: center;
  background: #f4f4f5;
  color: #71717a;
}

.gallery-card span {
  padding: 0 0.75rem 0.75rem;
  font-size: 0.875rem;
}

.gallery-lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: 2rem;
  background: rgb(0 0 0 / 0.86);
}

.gallery-lightbox img {
  max-width: min(100%, 1100px);
  max-height: 82vh;
  object-fit: contain;
}

.gallery-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
}

.gallery-empty {
  color: #71717a;
}

In a real app, you may replace this raw img with the image component from Next.js, Astro, or your CDN. The important part is unchanged: keep alt, width, height, and sizes in the data contract. Ask Claude Code to review whether fetchPriority fits your browser support target and whether the gallery should use an existing image abstraction in your repository.

Data Model and Use Cases

Keep the data outside the component. Even when images come from a CMS, normalize them into this shape before rendering. That makes filtering, tests, and broken-data handling much easier.

import type { GalleryImage } from "./ImageGallery";

export const galleryImages: GalleryImage[] = [
  {
    id: "case-study-dashboard",
    src: "/images/gallery/dashboard-960.webp",
    alt: "Analytics dashboard after Claude Code refactoring",
    width: 960,
    height: 640,
    category: "Case study",
    sources: [
      { width: 480, src: "/images/gallery/dashboard-480.webp" },
      { width: 960, src: "/images/gallery/dashboard-960.webp" },
      { width: 1440, src: "/images/gallery/dashboard-1440.webp" },
    ],
  },
  {
    id: "workshop-room",
    src: "/images/gallery/workshop-960.webp",
    alt: "Team workshop board with Claude Code review checklist",
    width: 960,
    height: 720,
    category: "Training",
  },
  {
    id: "product-shot",
    src: "/images/gallery/template-pack-960.webp",
    alt: "Claude Code template pack product preview",
    width: 960,
    height: 540,
    category: "Product",
  },
];

Use case one is a portfolio or case study page. The gallery should help a reader compare work, then continue to the relevant case study or consultation page. Do not label images as “project 1” or “screenshot”; describe the meaningful state shown in the image.

Use case two is an ecommerce or digital product page. Screenshots, usage examples, comparison images, and post-purchase previews reduce uncertainty. The mistake is loading all high-resolution images before the reader has even reached the buy button.

Use case three is training, events, or internal knowledge. Whiteboards, step screenshots, before-and-after screens, and error states become reusable learning material. For internal galleries, add a review item for customer names, personal data, and secrets in screenshots.

Use case four is an editorial article with diagrams. When an article contains several code examples, a concept diagram or screenshot gallery helps readers switch between explanation and implementation without losing context.

Pitfalls to Catch Before Publishing

The most common mistake is lazy-loading the image that appears immediately in the first viewport. That image often affects LCP, so the first visible item may need loading="eager" and fetchPriority="high". The opposite mistake is making every image eager and slowing down the initial render.

The second mistake is omitting width and height. Every image then changes the card height as it loads, which creates layout shift and makes the page feel unstable. Ask Claude Code to review for Cumulative Layout Shift risk before you merge.

The third mistake is treating alt text as an SEO keyword field. Alternative text is a replacement for the image when the image is unavailable or cannot be seen. “Claude Code image gallery React” is weaker than “Analytics dashboard after Claude Code refactoring.”

The fourth mistake is a mouse-only lightbox. It needs a named close button, Escape support, visible focus, and acceptable behavior on a narrow screen. If you need a strict focus trap, ask Claude Code whether a proven dialog primitive such as Radix UI or React Aria is safer than hand-rolling it.

The fifth mistake is leaving image operations undefined. A CMS user can upload a 6 MB PNG and ruin an otherwise good gallery. Put maximum dimensions, accepted formats, naming rules, and review checks into CLAUDE.md so Claude Code repeats them next time.

Verification Code and Review Flow

Before publishing, check category filtering, lightbox behavior, keyboard behavior, empty data, broken images, and mobile width. If you use Playwright, start with a thin test like this and expand it around your route names.

import { expect, test } from "@playwright/test";

test("image gallery filters and opens a lightbox", async ({ page }) => {
  await page.goto("/gallery");

  await expect(page.getByRole("region", { name: "Image gallery" })).toBeVisible();
  await page.getByRole("button", { name: "Training" }).click();
  await expect(page.getByRole("button", { name: /workshop/i })).toBeVisible();

  await page.getByRole("button", { name: /workshop/i }).click();
  await expect(page.getByRole("dialog")).toBeVisible();

  await page.keyboard.press("Escape");
  await expect(page.getByRole("dialog")).toBeHidden();
});

Do not ask Claude Code “does this look good?” Ask for a review with fixed criteria:

  • Initial image requests are reasonable.
  • srcset and sizes match actual layout widths.
  • Alternative text explains the image meaning.
  • Clickable cards are real buttons or links.
  • The page does not overflow at 375px.
  • Broken CMS data does not crash the route.
  • Screenshots do not expose private data or customer names.

A gallery should support a business path. A case study image should lead to a case study. A product image should lead to purchase details. A workshop image can lead to Claude Code training and consultation. For deeper technical reading, link to lazy loading images and React development with Claude Code.

If the page also has ads, do not let ad blocks break the gallery context. Revenue matters, but a reader who came to inspect screenshots should not fight layout shifts or intrusive placements. One clear CTA per viewport is easier to measure than three competing buttons.

Hands-on Verification Note

I tested this workflow by separating the prompt into data type, React implementation, CSS, and review criteria. The result was easier to review than one large “make it beautiful” request. Requiring width, height, and alt in the data model caught weak image entries early. The final manual pass used Chrome DevTools Network to check initial image requests, Lighthouse to inspect LCP and CLS, and a mobile viewport to operate the lightbox by keyboard and touch.

#Claude Code #image gallery #React #responsive #performance
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.