DEMO
CODE
<div class="demo-stage lbx-stage" id="lbx-stage">
<ul class="lbx-grid" id="lbx-grid" aria-label="ギャラリー"></ul>
<div class="lbx-view" id="lbx-view" role="dialog" aria-modal="true" aria-label="拡大表示" hidden>
<div class="lbx-photo" id="lbx-photo"></div>
<p class="lbx-cap" id="lbx-cap" aria-live="polite"></p>
<button type="button" class="lbx-btn lbx-prev" id="lbx-prev" aria-label="前の画像">‹</button>
<button type="button" class="lbx-btn lbx-next" id="lbx-next" aria-label="次の画像">›</button>
<button type="button" class="lbx-btn lbx-close" id="lbx-close" aria-label="閉じる">×</button>
</div>
</div>
.lbx-stage { position:relative; display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:320px; overflow:hidden; }
.lbx-grid { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(3,1fr); gap:.6rem; width:100%; max-width:420px; }
.lbx-thumb { display:block; width:100%; aspect-ratio:4/3; border:0; border-radius:10px; cursor:zoom-in; transition: transform .2s ease, filter .2s ease; }
.lbx-thumb:hover { transform:scale(1.03); filter:brightness(1.1); }
.lbx-thumb:focus-visible, .lbx-btn:focus-visible { outline:2px solid #fff; outline-offset:3px; }
.lbx-view { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.7rem; background:rgba(5,5,8,.9); animation: lbx-fade .2s ease both; }
.lbx-view[hidden] { display:none; }
.lbx-photo { width:min(76%, 380px); aspect-ratio:4/3; border-radius:12px; box-shadow:0 20px 50px -10px rgba(0,0,0,.8); animation: lbx-zoom .28s cubic-bezier(.22,1,.36,1) both; }
.lbx-cap { margin:0; color:#C9C9D1; font-size:.8rem; font-variant-numeric:tabular-nums; }
.lbx-btn { position:absolute; width:44px; height:44px; border:0; border-radius:50%; background:rgba(255,255,255,.1); color:#fff; font-size:1.4rem; line-height:1; cursor:pointer; }
.lbx-btn:hover { background:rgba(255,255,255,.2); }
.lbx-prev { left:.6rem; top:50%; margin-top:-22px; }
.lbx-next { right:.6rem; top:50%; margin-top:-22px; }
.lbx-close { top:.6rem; right:.6rem; font-size:1.1rem; }
@keyframes lbx-fade { from { opacity:0; } to { opacity:1; } }
@keyframes lbx-zoom { from { opacity:0; transform:scale(.9); } to { opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce) {
.lbx-view, .lbx-photo { animation:none; }
.lbx-thumb { transition-duration:.01ms; }
}
(function(){
var grid = document.getElementById('lbx-grid');
var view = document.getElementById('lbx-view');
var photo = document.getElementById('lbx-photo');
var cap = document.getElementById('lbx-cap');
if (!grid || !view || !photo) return;
var ITEMS = [
['Dawn', 'linear-gradient(135deg,#F9A8D4,#E91B89)'], ['Dusk', 'linear-gradient(135deg,#7B37A8,#1E1B4B)'],
['Sea', 'linear-gradient(135deg,#5BAADE,#0F766E)'], ['Forest', 'linear-gradient(135deg,#34D399,#065F46)'],
['Sand', 'linear-gradient(135deg,#FDE68A,#D97706)'], ['Night', 'linear-gradient(135deg,#334155,#0B0B10)']
];
var cur = 0, opener = null;
var thumbs = ITEMS.map(function(it, i){
var li = document.createElement('li');
var b = document.createElement('button');
b.type = 'button'; b.className = 'lbx-thumb'; b.style.background = it[1];
b.setAttribute('aria-label', it[0] + 'を拡大表示');
b.addEventListener('click', function(){ open(i, b); });
li.appendChild(b); grid.appendChild(li);
return b;
});
function show(i){
cur = (i + ITEMS.length) % ITEMS.length;
photo.style.background = ITEMS[cur][1];
photo.style.animation = 'none'; void photo.offsetWidth; photo.style.animation = '';
cap.textContent = ITEMS[cur][0] + ' ' + (cur + 1) + ' / ' + ITEMS.length;
}
function open(i, btn){ opener = btn; view.hidden = false; show(i); document.getElementById('lbx-close').focus(); }
function close(){ view.hidden = true; (thumbs[cur] || opener).focus(); }
document.getElementById('lbx-prev').addEventListener('click', function(){ show(cur - 1); });
document.getElementById('lbx-next').addEventListener('click', function(){ show(cur + 1); });
document.getElementById('lbx-close').addEventListener('click', close);
view.addEventListener('click', function(e){ if (e.target === view) close(); });
view.addEventListener('keydown', function(e){
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); show(cur - 1); }
else if (e.key === 'ArrowRight') { e.preventDefault(); show(cur + 1); }
else if (e.key === 'Tab') {
var f = view.querySelectorAll('button'), first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
});
})();
AI PROMPT
サムネイルを押すと拡大表示が開き、前後の画像へ送れるライトボックス(Lightbox)を作って。サムネイルは button 要素にして「〇〇を拡大表示」の aria-label を付け、grid で3列に並べる(ホバーで scale(1.03)・cursor: zoom-in)。拡大表示は role="dialog" aria-modal="true" の要素で、背景を暗くして .2秒でフェードイン、画像は scale(.9) から .28秒で拡大して現れる。前へ・次へ・閉じるボタン(44px 以上・aria-label 付き)を置き、←→キーでも送れるようにし、最後の次は最初へ戻る。キャプションに「名前 2 / 6」の形で位置を表示し、aria-live="polite" で読み上げに伝える。画像を切り替えるたびに拡大アニメーションを再生し直す(animation を none にして void offsetWidth で強制リフローしてから戻す)。開いたら閉じるボタンにフォーカスを移し、Tab と Shift+Tab でフォーカスがダイアログの外へ出ないように端で折り返す。Esc と背景クリックで閉じ、閉じたら最後に表示していた画像のサムネイルへフォーカスを戻す。@media (prefers-reduced-motion: reduce) ではアニメーションを無効化する。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
サムネイルを押すと、画面の上に画像を大きく表示し、前後の画像へ送れるようにする表示方法です。写真のポートフォリオ、ECの商品画像、施工事例の一覧などで、ページを移動せずに画像を大きく見せたい時の定番です。
当サイトの Crossfade Gallery は同じ場所で画像を切り替える演出、Expandable Card はカードがその場で詳細に広がる演出、Modal Scale Blur は情報を見せるモーダルです。本ページは画像の拡大と前後送りに特化した別テーマで、キーボードだけでも一覧から拡大・送り・閉じるまで操作できます。
品質を分けるのは操作の作法です。←→キーでの送り、Esc で閉じる、開いている間はフォーカスを外へ出さない、閉じたら一覧の位置へ戻す、の4つがそろうと、マウスを使わない人にも使いやすいギャラリーになります。
閉じたあと、どのサムネイルにフォーカスを戻すべきですか?
開いたサムネイルではなく、最後に表示していた画像のサムネイルへ戻すのが自然です。拡大表示の中で画像を送った場合、利用者の関心は最後に見ていた画像にあるためです。本実装は現在の番号のサムネイルへフォーカスを戻し、一覧の中の位置を見失わないようにしています。
実際の写真を使う場合に気をつけることは?
拡大表示では大きな画像を読み込むため、サムネイルには小さい画像、拡大表示には大きい画像と、別々のファイルを用意してください。拡大用の画像は開いた時に初めて読み込み、読み込み中は同じ大きさの枠を先に表示しておくと、表示の途中でレイアウトが跳ねません。画像には内容を表す代替テキストも必ず付けてください。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。