Use Cases (更新: 2026/6/2)

Claude Codeで画像ギャラリーを作る実践ガイド

Claude Codeで高速でアクセシブルな画像ギャラリーを作る手順。Reactコード、失敗例、検証観点まで解説。

Claude Codeで画像ギャラリーを作る実践ガイド

画像ギャラリーは「きれいに並べる」だけでは足りない

Claude Codeで画像ギャラリーを作るとき、最初に決めるべきことはレイアウトではありません。誰が、どの端末で、何枚の画像を、何のために見るのかです。ポートフォリオなら作品の比較、ECなら購入前の不安解消、社内ナレッジなら証跡の検索性が主目的になります。ここを曖昧にしたまま「Pinterest風にして」と頼むと、見た目は整っても、画像が重い、代替テキストが弱い、ライトボックスがキーボードで閉じられない、といった公開後の問題が残ります。

この記事では、要件の渡し方、React実装、CSS、検証、落とし穴をまとめます。関連する基礎として、画像変換はClaude Code画像処理、読み込み速度はパフォーマンス最適化、キーボード操作はアクセシビリティ実装も合わせて確認してください。外部の一次情報としては、Claude Code公式ドキュメント、MDNのレスポンシブ画像Lazy loading、W3CのWCAG 2.2を参照します。

実案件では、いきなりコンポーネントを書かせず、まず「画像データの型」「読み込み戦略」「失敗時の表示」「レビュー観点」を固定します。harnessはエージェントの足場という意味で、ここではClaude Codeが迷わない作業範囲を示すものです。

Claude Codeに渡す依頼文

最初の依頼は、派手なUI指定よりも制約を多くします。次のまま貼り付けても使えます。

Reactで画像ギャラリーを実装してください。
目的は、記事・制作実績・商品写真を高速に閲覧できるUIです。

条件:
- 既存のルーティングやデザインシステムを壊さない
- 画像データは型定義し、alt、width、height、categoryを必須にする
- CSS Gridでレスポンシブに並べる
- srcset、sizes、loading、fetchPriorityを使い分ける
- クリックでライトボックスを開き、Escapeで閉じられる
- 空配列、画像読み込み失敗、長いalt、スマホ幅を考慮する
- 実装後に変更ファイル、テスト観点、残ったリスクを説明する

疑似コードではなく、コピーして動くReact/TypeScriptとCSSを返してください。

この依頼文の狙いは、Claude Codeの出力を「きれいな完成画面」ではなく「レビューできる差分」に寄せることです。特にwidthheightを必須にする指定は、読み込み中のレイアウトずれを防ぐために効きます。altも後付けにすると雑になりやすいため、データの時点で必須にしておきます。

実装の全体像

ギャラリーの処理は、画像ファイルを表示するだけに見えて、実際にはいくつかの責務に分かれます。Claude Codeにはこの図を渡してから実装させると、関心の分離が安定します。

flowchart LR
  A["元画像"] --> B["サイズ別画像を用意"]
  B --> C["GalleryImageの配列"]
  C --> D["カテゴリ絞り込み"]
  D --> E["CSS Grid表示"]
  E --> F["ライトボックス"]
  E --> G["Lighthouseと手動確認"]
判断項目安全な初期値後で見直す条件
レイアウトCSS Grid高さが大きくばらつく写真が多い
遅延読み込み下部画像だけlazyファーストビュー画像まで遅い
画像サイズ480/960/1440px程度Retinaや大型モニター比率が高い
ライトボックス最小機能から開始購入・資料請求導線に直結する

最初からmasonry専用ライブラリを入れないのは、「本当に不規則な高さが必要か」を確認したいからです。CSS Gridで十分なケースは多く、レビュー対象も小さくできます。

コピーして使えるReact実装

次は、ViteやNext.jsのクライアントコンポーネントにそのまま移植しやすい形です。Next.jsでnext/imageを使う場合でも、まずこの構造でデータとUIの責務を分けてから置き換えると安全です。

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;
}

実務では、この素のimgをNext.jsやAstroの画像コンポーネントに置き換えることもあります。それでも、altwidthheightsizesをデータ側で揃える考え方は変わりません。Claude CodeにはfetchPriorityの対象ブラウザ対応と、既存画像コンポーネントへ寄せるべきかを確認させます。

データ例と3つ以上のユースケース

データはUIの外に置きます。CMSから取る場合でも、最終的にこの形へ正規化するとテストしやすくなります。

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",
  },
];

1つ目のユースケースは、制作実績やポートフォリオです。見た目の印象だけでなく、各画像から事例記事や問い合わせ導線へ進める必要があります。画像のaltには「作品1」ではなく、何を示す画像かを書きます。

2つ目は、ECやデジタル商品の詳細ページです。サムネイル、利用イメージ、比較画像、購入後の画面を並べると、読者は購入前の不安を減らせます。ただし最初の表示で全画像を高解像度読み込みすると、コンバージョン前に離脱されます。

3つ目は、研修・イベント・社内ナレッジです。ホワイトボード、手順のスクリーンショット、Before/After、エラー画面をギャラリー化すると、説明の再利用性が上がります。社内向けでは、画像に個人情報や顧客名が残っていないかをClaude Codeのチェックリストに必ず含めます。

4つ目は、記事メディアの図解ギャラリーです。コード例が多い記事では、概念図、処理フロー、検証スクリーンショットが理解を助けます。

失敗例と落とし穴

一番多い失敗は、ファーストビューの大きな画像までloading="lazy"にしてしまうことです。表示直後に見える画像はLCP、つまり読み込み体験の中心になりやすいため、最初の1枚はeagerfetchPriority="high"を検討します。反対に、ページ下部の画像を全部eagerにすると初期表示が遅くなります。

次に多いのは、widthheightを省略してレイアウトが跳ねる問題です。画像が読み込まれるたびにカードの高さが変わると、読者はクリック位置を失います。Claude Codeには「Cumulative Layout Shiftを起こしそうな箇所を指摘して」と依頼してください。

3つ目は、altをSEO用キーワードの詰め込みにしてしまうことです。altは検索エンジンだけでなく、画像を見られない読者への説明です。「Claude Code 画像 ギャラリー React」ではなく、「Claude Codeで生成したレビュー一覧画面」のように画像の意味を書きます。

4つ目は、ライトボックスをマウス専用にすることです。Escapeで閉じる、閉じるボタンにラベルを付ける、背景クリックで閉じる、フォーカス順を確認する。ここまでが最低ラインです。厳密なフォーカストラップが必要な場合は、Radix UIやReact Ariaのような実績あるライブラリも候補にします。

5つ目は、画像生成・圧縮・アップロードの運用を決めないことです。CMSに巨大なPNGが混ざるだけで遅くなります。「画像サイズの上限」「許可する形式」「命名規則」「差分レビュー項目」をCLAUDE.mdへ追記しておくと、次回から安定します。

検証コードとレビュー手順

公開前には、最低でもカテゴリ切り替え、ライトボックス、キーボード、空データ、スマホ幅を確認します。Playwrightを使っているなら、次のような薄いテストから始められます。

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();
});

Claude Codeへのレビュー依頼は「良さそうですか?」では弱いです。次の観点を固定すると、見逃しが減ります。

  • 画像の初期読み込みが重すぎないか
  • srcsetsizesの指定がレイアウト幅と合っているか
  • altが画像の意味を説明しているか
  • クリック可能領域がボタンとして扱われているか
  • 375px幅で横スクロールしないか
  • CMSの壊れたデータで落ちないか
  • 画像権利、個人情報、顧客名の写り込みがないか

CTAと内部リンクの置き方

画像ギャラリーは、見栄えのためだけに置くと収益に結びつきません。事例画像なら事例記事へ、商品画像なら購入ページへ、研修風景ならClaude Code研修・相談へ自然に進める導線を置きます。実装を深掘りする読者には画像遅延読み込み、UI全体を整えたい読者にはReact開発ガイドもつなげると回遊が自然です。

広告を置くページでも、画像を見たい読者の意図を壊す配置は避けます。CTAは「無料相談」「テンプレートを見る」「事例を読む」のように1画面1主目的へ絞るほうが、計測も改善もやりやすくなります。

実際に試した結果

この記事の構成で、Claude Codeに「データ型、React実装、CSS、レビュー観点」を分けて依頼すると、一度に大きなUIを作らせるよりレビューが楽でした。特にwidthheightaltをデータ必須にしたことで、後から画像だけを追加してもレイアウトずれと説明不足を見つけやすくなりました。最終確認ではChrome DevToolsのNetworkで初期読み込み枚数を見て、LighthouseでLCPとCLSを確認し、スマホ幅でライトボックスを手動操作しました。

#Claude Code #画像ギャラリー #React #responsive #performance
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。