ライトボックス
サムネイルをクリックすると暗幕オーバーレイに写真を拡大表示。前後送りの矢印、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(); });
})();
🤖 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。