ライトボックス

サムネイルをクリックすると暗幕オーバーレイに写真を拡大表示。前後送りの矢印、Escキーや×ボタンでの終了、キャプション表示まで揃えた定番のライトボックスUIです。

#css#js#gallery

ライブデモ

コード

HTML
<!-- ライトボックス: サムネイルグリッド→クリックで暗幕オーバーレイに拡大表示。前/次送り・Esc/×で閉じる -->
<section class="lb" aria-label="作品ギャラリー">
  <ul class="lb__grid" role="list">
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="0" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1015/600/400" alt="断崖から見下ろすフィヨルド" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="1" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1025/600/400" alt="毛布にくるまる犬" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="2" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1039/600/400" alt="緑の渓谷に流れる滝" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="3" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1043/600/400" alt="渓谷の森と静かな水面" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="4" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1035/600/400" alt="虹のかかる渓谷の滝" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="5" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1074/600/400" alt="野生のライオンの横顔" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
  </ul>
</section>

<div class="lb__overlay" id="lbOverlay" role="dialog" aria-modal="true" aria-label="画像ビューア" hidden>
  <button class="lb__ctrl lb__close" id="lbClose" type="button" aria-label="閉じる">✕</button>
  <button class="lb__ctrl lb__prev" id="lbPrev" type="button" aria-label="前の画像">‹</button>
  <figure class="lb__figure">
    <img class="lb__full" id="lbImg" src="" alt="">
    <figcaption class="lb__caption" id="lbCaption"></figcaption>
  </figure>
  <button class="lb__ctrl lb__next" id="lbNext" type="button" aria-label="次の画像">›</button>
  <p class="lb__counter" id="lbCounter" aria-hidden="true"></p>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 0%, #24262d, #101115 72%);
  font-family: "Segoe UI", system-ui, sans-serif;
  padding: 20px;
}

.lb { width: min(94vw, 640px); }
.lb__grid {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(6px, 1.6vw, 12px);
}
.lb__cell { aspect-ratio: 3 / 2; }
.lb__thumb {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 10px;
  overflow: hidden;
  cursor: zoom-in;
  background: #1a1b20;
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, .6);
}
.lb__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1.02);
  transition: transform .45s cubic-bezier(.2, .7, .2, 1), filter .3s ease;
  filter: saturate(.92);
}
.lb__mark {
  position: absolute;
  right: 8px;
  bottom: 8px;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 13px;
  color: #fff;
  background: rgba(10, 11, 14, .55);
  backdrop-filter: blur(3px);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .25s ease, transform .25s ease;
}
.lb__thumb:hover img,
.lb__thumb:focus-visible img { transform: scale(1.09); filter: saturate(1.05); }
.lb__thumb:hover .lb__mark,
.lb__thumb:focus-visible .lb__mark { opacity: 1; transform: translateY(0); }
.lb__thumb:focus-visible {
  outline: 2px solid #8ab4ff;
  outline-offset: 3px;
}

/* オーバーレイ(暗幕) */
.lb__overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(4px, 2vw, 20px);
  padding: clamp(14px, 4vw, 40px);
  background: rgba(8, 9, 12, .93);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity .28s ease;
}
.lb__overlay[hidden] { display: none; }
.lb__overlay.is-open { opacity: 1; }

.lb__ctrl {
  border: 0;
  background: rgba(255, 255, 255, .08);
  color: #fff;
  cursor: pointer;
  border-radius: 50%;
  width: 44px;
  height: 44px;
  font-size: 20px;
  line-height: 1;
  display: grid;
  place-items: center;
  transition: background .2s ease, transform .2s ease;
}
.lb__ctrl:hover { background: rgba(255, 255, 255, .18); }
.lb__ctrl:active { transform: scale(.92); }
.lb__ctrl:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }

.lb__close {
  position: absolute;
  top: clamp(10px, 3vw, 24px);
  right: clamp(10px, 3vw, 24px);
  z-index: 2;
}
.lb__prev { grid-column: 1; }
.lb__next { grid-column: 3; }

.lb__figure {
  grid-column: 2;
  margin: 0;
  min-width: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  transform: scale(.94);
  opacity: 0;
  transition: transform .32s cubic-bezier(.2, .7, .2, 1), opacity .32s ease;
}
.lb__overlay.is-open .lb__figure { transform: scale(1); opacity: 1; }
.lb__full {
  max-width: min(80vw, 720px);
  max-height: 62vh;
  width: auto;
  height: auto;
  border-radius: 12px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .8);
  display: block;
}
.lb__caption {
  color: #e7e8ee;
  font-size: 13px;
  letter-spacing: .02em;
  text-align: center;
  opacity: .85;
}
.lb__counter {
  position: absolute;
  bottom: clamp(10px, 3vw, 22px);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  color: #9aa0ad;
  font-size: 11px;
  letter-spacing: .18em;
}

@media (max-width: 480px) {
  .lb__grid { grid-template-columns: repeat(2, 1fr); }
  .lb__overlay { grid-template-columns: 1fr; }
  .lb__prev, .lb__next { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; }
  .lb__prev { left: 8px; }
  .lb__next { right: 8px; }
}
JavaScript
// ライトボックス: サムネイル一覧を配列化し、クリック/キーボードで拡大表示・前後送りする
(() => {
  const thumbs = Array.from(document.querySelectorAll(".lb__thumb"));
  const overlay = document.getElementById("lbOverlay");
  const img = document.getElementById("lbImg");
  const caption = document.getElementById("lbCaption");
  const counter = document.getElementById("lbCounter");
  const closeBtn = document.getElementById("lbClose");
  const prevBtn = document.getElementById("lbPrev");
  const nextBtn = document.getElementById("lbNext");
  if (!thumbs.length || !overlay || !img || !closeBtn || !prevBtn || !nextBtn) return; // null安全

  // サムネイルの img から src/alt を拾って拡大表示用のデータを作る
  const items = thumbs.map((btn) => {
    const src = btn.querySelector("img");
    return {
      src: src ? src.getAttribute("src") : "",
      alt: src ? src.getAttribute("alt") : "",
    };
  });

  let current = 0;
  let lastFocused = null;
  const focusables = [closeBtn, prevBtn, nextBtn];

  const render = () => {
    const it = items[current];
    if (!it) return;
    img.src = it.src;
    img.alt = it.alt;
    if (caption) caption.textContent = it.alt;
    if (counter) counter.textContent = `${current + 1} / ${items.length}`;
  };

  const open = (index) => {
    current = (index + items.length) % items.length;
    lastFocused = document.activeElement;
    render();
    overlay.hidden = false;
    // reflow を挟んでからフェードインさせる
    requestAnimationFrame(() => overlay.classList.add("is-open"));
    closeBtn.focus();
    document.addEventListener("keydown", onKeydown, true);
  };

  const close = () => {
    overlay.classList.remove("is-open");
    document.removeEventListener("keydown", onKeydown, true);
    window.setTimeout(() => { overlay.hidden = true; }, 280);
    if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
  };

  const step = (delta) => { current = (current + delta + items.length) % items.length; render(); };

  const onKeydown = (e) => {
    if (e.key === "Escape") { e.preventDefault(); close(); return; }
    if (e.key === "ArrowRight") { e.preventDefault(); step(1); return; }
    if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); return; }
    if (e.key === "Tab") {
      // オーバーレイ内でフォーカスを循環させる(簡易フォーカストラップ)
      const idx = focusables.indexOf(document.activeElement);
      e.preventDefault();
      const dir = e.shiftKey ? -1 : 1;
      const next = focusables[(idx + dir + focusables.length) % focusables.length];
      if (next) next.focus();
    }
  };

  thumbs.forEach((btn, i) => {
    btn.addEventListener("click", () => open(i));
  });
  closeBtn.addEventListener("click", close);
  prevBtn.addEventListener("click", () => step(-1));
  nextBtn.addEventListener("click", () => step(1));
  // 暗幕(フィギュア以外)クリックで閉じる
  overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ライトボックス」の効果を追加してください。

# 追加してほしい効果
ライトボックス(画像 & ギャラリー)
サムネイルをクリックすると暗幕オーバーレイに写真を拡大表示。前後送りの矢印、Escキーや×ボタンでの終了、キャプション表示まで揃えた定番のライトボックスUIです。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ライトボックス: サムネイルグリッド→クリックで暗幕オーバーレイに拡大表示。前/次送り・Esc/×で閉じる -->
<section class="lb" aria-label="作品ギャラリー">
  <ul class="lb__grid" role="list">
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="0" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1015/600/400" alt="断崖から見下ろすフィヨルド" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="1" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1025/600/400" alt="毛布にくるまる犬" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="2" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1039/600/400" alt="緑の渓谷に流れる滝" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="3" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1043/600/400" alt="渓谷の森と静かな水面" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="4" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1035/600/400" alt="虹のかかる渓谷の滝" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
    <li class="lb__cell">
      <button class="lb__thumb" type="button" data-index="5" aria-haspopup="dialog">
        <img src="https://picsum.photos/id/1074/600/400" alt="野生のライオンの横顔" loading="lazy">
        <span class="lb__mark" aria-hidden="true">⤢</span>
      </button>
    </li>
  </ul>
</section>

<div class="lb__overlay" id="lbOverlay" role="dialog" aria-modal="true" aria-label="画像ビューア" hidden>
  <button class="lb__ctrl lb__close" id="lbClose" type="button" aria-label="閉じる">✕</button>
  <button class="lb__ctrl lb__prev" id="lbPrev" type="button" aria-label="前の画像">‹</button>
  <figure class="lb__figure">
    <img class="lb__full" id="lbImg" src="" alt="">
    <figcaption class="lb__caption" id="lbCaption"></figcaption>
  </figure>
  <button class="lb__ctrl lb__next" id="lbNext" type="button" aria-label="次の画像">›</button>
  <p class="lb__counter" id="lbCounter" aria-hidden="true"></p>
</div>

【CSS】
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 0%, #24262d, #101115 72%);
  font-family: "Segoe UI", system-ui, sans-serif;
  padding: 20px;
}

.lb { width: min(94vw, 640px); }
.lb__grid {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(6px, 1.6vw, 12px);
}
.lb__cell { aspect-ratio: 3 / 2; }
.lb__thumb {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 10px;
  overflow: hidden;
  cursor: zoom-in;
  background: #1a1b20;
  box-shadow: 0 6px 16px -8px rgba(0, 0, 0, .6);
}
.lb__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(1.02);
  transition: transform .45s cubic-bezier(.2, .7, .2, 1), filter .3s ease;
  filter: saturate(.92);
}
.lb__mark {
  position: absolute;
  right: 8px;
  bottom: 8px;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 13px;
  color: #fff;
  background: rgba(10, 11, 14, .55);
  backdrop-filter: blur(3px);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .25s ease, transform .25s ease;
}
.lb__thumb:hover img,
.lb__thumb:focus-visible img { transform: scale(1.09); filter: saturate(1.05); }
.lb__thumb:hover .lb__mark,
.lb__thumb:focus-visible .lb__mark { opacity: 1; transform: translateY(0); }
.lb__thumb:focus-visible {
  outline: 2px solid #8ab4ff;
  outline-offset: 3px;
}

/* オーバーレイ(暗幕) */
.lb__overlay {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(4px, 2vw, 20px);
  padding: clamp(14px, 4vw, 40px);
  background: rgba(8, 9, 12, .93);
  backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity .28s ease;
}
.lb__overlay[hidden] { display: none; }
.lb__overlay.is-open { opacity: 1; }

.lb__ctrl {
  border: 0;
  background: rgba(255, 255, 255, .08);
  color: #fff;
  cursor: pointer;
  border-radius: 50%;
  width: 44px;
  height: 44px;
  font-size: 20px;
  line-height: 1;
  display: grid;
  place-items: center;
  transition: background .2s ease, transform .2s ease;
}
.lb__ctrl:hover { background: rgba(255, 255, 255, .18); }
.lb__ctrl:active { transform: scale(.92); }
.lb__ctrl:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }

.lb__close {
  position: absolute;
  top: clamp(10px, 3vw, 24px);
  right: clamp(10px, 3vw, 24px);
  z-index: 2;
}
.lb__prev { grid-column: 1; }
.lb__next { grid-column: 3; }

.lb__figure {
  grid-column: 2;
  margin: 0;
  min-width: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  transform: scale(.94);
  opacity: 0;
  transition: transform .32s cubic-bezier(.2, .7, .2, 1), opacity .32s ease;
}
.lb__overlay.is-open .lb__figure { transform: scale(1); opacity: 1; }
.lb__full {
  max-width: min(80vw, 720px);
  max-height: 62vh;
  width: auto;
  height: auto;
  border-radius: 12px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, .8);
  display: block;
}
.lb__caption {
  color: #e7e8ee;
  font-size: 13px;
  letter-spacing: .02em;
  text-align: center;
  opacity: .85;
}
.lb__counter {
  position: absolute;
  bottom: clamp(10px, 3vw, 22px);
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  color: #9aa0ad;
  font-size: 11px;
  letter-spacing: .18em;
}

@media (max-width: 480px) {
  .lb__grid { grid-template-columns: repeat(2, 1fr); }
  .lb__overlay { grid-template-columns: 1fr; }
  .lb__prev, .lb__next { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; }
  .lb__prev { left: 8px; }
  .lb__next { right: 8px; }
}

【JavaScript】
// ライトボックス: サムネイル一覧を配列化し、クリック/キーボードで拡大表示・前後送りする
(() => {
  const thumbs = Array.from(document.querySelectorAll(".lb__thumb"));
  const overlay = document.getElementById("lbOverlay");
  const img = document.getElementById("lbImg");
  const caption = document.getElementById("lbCaption");
  const counter = document.getElementById("lbCounter");
  const closeBtn = document.getElementById("lbClose");
  const prevBtn = document.getElementById("lbPrev");
  const nextBtn = document.getElementById("lbNext");
  if (!thumbs.length || !overlay || !img || !closeBtn || !prevBtn || !nextBtn) return; // null安全

  // サムネイルの img から src/alt を拾って拡大表示用のデータを作る
  const items = thumbs.map((btn) => {
    const src = btn.querySelector("img");
    return {
      src: src ? src.getAttribute("src") : "",
      alt: src ? src.getAttribute("alt") : "",
    };
  });

  let current = 0;
  let lastFocused = null;
  const focusables = [closeBtn, prevBtn, nextBtn];

  const render = () => {
    const it = items[current];
    if (!it) return;
    img.src = it.src;
    img.alt = it.alt;
    if (caption) caption.textContent = it.alt;
    if (counter) counter.textContent = `${current + 1} / ${items.length}`;
  };

  const open = (index) => {
    current = (index + items.length) % items.length;
    lastFocused = document.activeElement;
    render();
    overlay.hidden = false;
    // reflow を挟んでからフェードインさせる
    requestAnimationFrame(() => overlay.classList.add("is-open"));
    closeBtn.focus();
    document.addEventListener("keydown", onKeydown, true);
  };

  const close = () => {
    overlay.classList.remove("is-open");
    document.removeEventListener("keydown", onKeydown, true);
    window.setTimeout(() => { overlay.hidden = true; }, 280);
    if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
  };

  const step = (delta) => { current = (current + delta + items.length) % items.length; render(); };

  const onKeydown = (e) => {
    if (e.key === "Escape") { e.preventDefault(); close(); return; }
    if (e.key === "ArrowRight") { e.preventDefault(); step(1); return; }
    if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); return; }
    if (e.key === "Tab") {
      // オーバーレイ内でフォーカスを循環させる(簡易フォーカストラップ)
      const idx = focusables.indexOf(document.activeElement);
      e.preventDefault();
      const dir = e.shiftKey ? -1 : 1;
      const next = focusables[(idx + dir + focusables.length) % focusables.length];
      if (next) next.focus();
    }
  };

  thumbs.forEach((btn, i) => {
    btn.addEventListener("click", () => open(i));
  });
  closeBtn.addEventListener("click", close);
  prevBtn.addEventListener("click", () => step(-1));
  nextBtn.addEventListener("click", () => step(1));
  // 暗幕(フィギュア以外)クリックで閉じる
  overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。