コピペで完結!ZOZOTOWN風スライドギャラリー(自動で動くサムネ表示)【カルーセル】
html/css/js
ZOZOTOWN風スライドギャラリー(自動で動くサムネ表示)【カルーセル】
ECサイトなどでよく見かける、横にスライドする「ZOZOTOWN風カルーセル」を、コピペだけで簡単に使える形でご紹介します。
自動でスライドが動き、無限ループ・ドット連動・左右の矢印・タッチスワイプ・キーボード操作にも対応。
スマホではスライドが中央に綺麗に収まるよう調整されています。
ページ下部で実際の動きを確認したあと、「CSS」「HTML」「JS」のコードをそのままコピーして貼り付けるだけでOK。
画像サイズは16:9(例:1200×675px)を推奨。
1枚目の画像は loading="eager"、2枚目以降は loading="lazy" にすると表示もスムーズです。
編集するのは「リンク先URL」「画像URL(src)」「代替テキスト(alt)」「タイトル」「説明文」だけ。
そのほかの構造やクラス名は変更せずに使うのがおすすめです。
コードについて
本記事のコードは AI(ChatGPT)による生成をベースに作成・調整しています。ご利用の環境でテストの上ご使用ください。
免責
本コードの利用に伴う不具合・損害について、当サイトは責任を負いません。自己責任にてご利用ください。
デモ
PC表示デモ
スマホ表示イメージ
※スマホでの見え方イメージ(実機参考)
コードをコピーして使おう!
/* ==========================================
Netflix風スライドギャラリー CSS
スコープ:#gallery-section(推奨)
------------------------------------------
▼このコードはWordPressの「カスタムHTML」に
そのまま貼り付けて動作します。
▼画像・ドット・ボタン・スライド動作をすべて自動制御。
▼ドットが楕円に見える現象もChromeスマホで修正済み。
▼クラス名・セレクタは変更しないでください(JSと連動)
========================================== */
/* ▼ギャラリー全体の枠を定義(全体を中央に表示) */
#gallery-section .gallery-wrap {
position: relative; /* ボタンやドットの基準位置(absolute配置の基準) */
width: 100%; /* コンテナ全幅に拡張(親幅いっぱい) */
max-width: 1200px; /* 横幅の上限を設定(巨大画面での可読性確保) */
overflow: hidden; /* スライドがはみ出さないように隠す(視覚的な切り抜き) */
border-radius: 16px; /* 外枠の角丸(デザイン性UP) */
margin: 0 auto; /* 横中央寄せ(左右オートマージン) */
background: #f3f4f6; /* 背景色を淡いグレーに(画像がない時の見栄え) */
}
/* ▼スライドを横並びに配置するトラック */
#gallery-section .gallery-track {
display: flex; /* 横並びで並べる(子要素を横方向フロー) */
gap: var(--gap, 24px); /* スライド間の隙間を設定(CSSカスタムプロパティ対応) */
transition: transform 0.6s ease; /* スライド移動時のアニメーション(JSからtransform制御) */
will-change: transform; /* GPU最適化で滑らかに(変化前提のプロパティを宣言) */
}
/* ▼1枚ごとのスライドカード設定 */
#gallery-section .gallery-card {
flex: 0 0 calc((100% - (var(--gap, 24px) * 2)) / 3); /* 同時に3枚表示(gapぶん差し引き) */
aspect-ratio: 16 / 9; /* 比率16:9を固定(レスポンシブでも崩れない) */
border-radius: 16px; /* カードの角丸(親と揃える) */
overflow: hidden; /* 画像がはみ出さないように(角丸を適用) */
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15); /* ふんわり影(浮遊感) */
background: #000; /* 背景黒で統一(読み込み中のチラつき軽減) */
position: relative; /* キャプション配置のため(absoluteの基準に) */
container-type: inline-size; /* コンテナクエリ対応(cqw等の相対単位で可読性UP) */
}
/* ▼スライド画像設定 */
#gallery-section .gallery-card img {
width: 100%; height: 100%; /* カード全体にフィット(余白なし) */
object-fit: cover; /* 比率を維持してトリミング(横長・縦長どちらも安定) */
display: block; /* 画像の下の隙間を除去(inline要素由来の余白対策) */
transition: opacity 0.3s ease; /* フェード時に滑らかに(将来の演出にも対応) */
}
/* ▼キャプションエリア(下部の説明帯) */
#gallery-section .caption {
position: absolute; bottom: 0; left: 0; right: 0; /* 下辺に全面重ねる */
padding: min(20px, 2.4cqw) min(16px, 2cqw); /* 内側余白を動的調整(コンテナ幅基準) */
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); /* 下部グラデーション(文字の可読性確保) */
color: #fff; /* 白文字で可読性を確保(暗背景前提) */
}
/* ▼キャプションタイトル(太字タイトル部分) */
#gallery-section .caption-title {
font-weight: 700; /* 太字で強調(視認性UP) */
font-size: clamp(14px, 2.8cqw, 20px); /* 幅に応じて可変サイズ(最小/最大ガード) */
line-height: 1.25; /* 高さを少し詰める(詰まり過ぎない範囲) */
margin: 0 0 4px; /* タイトル下に少し余白(本文と分離) */
}
/* ▼キャプション本文(小さめテキスト) */
#gallery-section .caption-text {
font-size: clamp(12px, 2.2cqw, 14px); /* レスポンシブ文字サイズ(最小/最大ガード) */
opacity: 0.9; /* 少し薄くして主張を抑える(タイトルとのコントラスト) */
line-height: 1.5; /* 行間を確保(可読性) */
margin: 0; /* 余白をリセット(外側の余白管理を簡潔に) */
}
/* ▼左右のナビゲーションボタン(❮ ❯) */
#gallery-section .gallery-btn {
position: absolute; top: 50%;
transform: translateY(-50%); /* ボタンを縦中央に(上下の中心揃え) */
background: rgba(0, 0, 0, 0.4); /* 半透明の黒背景(背景と馴染む) */
color: #fff; /* 白文字(視認性) */
border: none; /* 枠線なし(フラット) */
border-radius: 50%; /* 丸いボタン(アイコンボタン感) */
width: 42px; height: 42px; /* サイズ(タップもしやすい) */
cursor: pointer; /* マウスカーソルを指に(クリックできることを示す) */
font-size: 20px; /* アイコンサイズ(❮❯が見やすい) */
z-index: 10; /* 他の要素より前に(画像やキャプションより優先) */
transition: background 0.3s ease; /* ホバー時に色が変化(インタラクションの手応え) */
}
#gallery-section .gallery-btn:hover { background: rgba(0,0,0,0.6); } /* ホバー濃く(PC操作時) */
#gallery-section .gallery-btn.prev { left: 10px; } /* 左ボタン位置(内側に10px) */
#gallery-section .gallery-btn.next { right: 10px; } /* 右ボタン位置(内側に10px) */
/* ▼ドットナビゲーション(下の丸いインジケータ) */
#gallery-section .dots {
margin-top: 12px; /* 上の余白(ギャラリーとの間隔) */
display: flex; /* 横並び配置(中央寄せしやすい) */
justify-content: center; /* 中央寄せ(左右均等) */
gap: 10px; /* 間隔(ドット同士の距離) */
}
/* ▼ドットボタンの基本スタイル */
#gallery-section .dot {
appearance: none; /* ブラウザ既定ボタン装飾を無効(環境差吸収) */
border: none; /* 枠線なし(フラット) */
width: 12px; height: 12px; /* 正円サイズ(PC時の基準) */
border-radius: 50%; /* 丸くする(正円の見た目) */
background: #9ca3af; /* グレー色(非アクティブ色) */
cursor: pointer; /* カーソル変更(クリック可能) */
transition: background 0.2s ease, transform 0.12s ease; /* 色と拡大アニメ(操作感) */
/* ▼Chromeスマホなどで楕円化を防ぐ設定(UA既定のline-height等の影響を排除) */
padding: 0; margin: 0; line-height: 0; font-size: 0;
display: inline-block; flex-shrink: 0; aspect-ratio: 1 / 1; /* 幅=高さを強制 */
}
#gallery-section .dot.active { background: #000; } /* アクティブ時に黒く(現在位置を示す) */
/* ▼マウスホバー時のアニメーション(PC用) */
@media (hover:hover){
#gallery-section .dot:hover { transform: scale(1.06); } /* 少し拡大(選択候補の強調) */
}
/* ▼フォーカスリング(キーボード操作用・アクセシビリティ対応) */
#gallery-section .dot:focus-visible {
outline: 2px solid #000; /* 明確なフォーカス枠(コントラスト確保) */
outline-offset: 2px; /* 要素から少し離して見やすく */
}
/* ▼スマホレイアウト調整(画面幅768px以下を想定) */
@media (max-width:768px){
#gallery-section .gallery-track { --gap: 12px; } /* スライド間を狭く(小画面向け) */
#gallery-section .gallery-card { flex: 0 0 80%; } /* 1枚を大きく中央に(Netflix風) */
#gallery-section .dots { margin-top:10px; gap:6px; } /* ドット間を詰める(密度最適化) */
/* ▼ドット(正円保証・楕円防止完全版) */
#gallery-section .dot {
width: 7px; height: 7px; background: #b0b8c0; border-radius: 50%; /* 小さめに最適化 */
appearance: none; border: 0; padding: 0; margin: 0; /* 既定装飾をリセット */
line-height: 0; font-size: 0; display: inline-block; /* 行高・テキスト影響を排除 */
flex-shrink: 0; aspect-ratio: 1 / 1; /* 1:1を強制して楕円化を防止 */
transition: background 0.2s ease, transform 0.12s ease; /* PC同様の操作感 */
cursor: pointer; /* タップ対象であることを明示 */
}
#gallery-section .dot.active { background:#000; } /* アクティブ時に黒(視認性) */
#gallery-section .gallery-btn { width:36px; height:36px; } /* ボタン小さく(指で押しやすいサイズを維持) */
#gallery-section .gallery-wrap { border-radius:14px; } /* 外枠少し角丸に(小画面での見栄え調整) */
#gallery-section .caption { padding:14px 10px; } /* キャプション余白調整(情報量とのバランス) */
}
<!-- ==========================================
Netflix風スライドギャラリー HTML構造
スコープ:#gallery-section
------------------------------------------
▼ このHTMLをWordPressのカスタムHTMLに貼り付ければOK
▼ CSSとJSを組み合わせることで自動スライドが動作します
=========================================== -->
<section id="gallery-section"> <!-- ギャラリー全体を包むセクション(スコープ) -->
<div class="gallery-wrap" id="gallery"> <!-- JSで制御されるラッパー。id="gallery" が必須 -->
<div class="gallery-track"> <!-- 横並びにスライドを配置するトラック領域 -->
<!-- ▼ 各スライド(画像+キャプション) -->
<!-- ▼ 以下、1枚目のみ編集ポイントを明記しています。2枚目以降は同様に
「リンク先URL / 画像URL(src) / altテキスト / タイトル / 説明文」
を差し替えてください。構造やクラス名は変更しないでください。 -->
<div class="gallery-card"> <!-- スライド1枚目のカード(構造は変更不可) -->
<a href="https://example.com/link1" target="_blank" rel="noopener">
<!-- 編集OK:href のURL(リンク先)を差し替え可。target, rel はそのまま推奨 -->
<img src="https://picsum.photos/seed/park/1200/675" alt="公園の写真" loading="eager">
<!-- 編集OK:
・src(画像URL)を差し替え可(推奨サイズ 1200×675 程度 / 16:9)
・alt(代替テキスト)を画像内容に合わせて変更可
・loading は 1枚目は eager、2枚目以降は lazy を推奨(UX/パフォーマンス) -->
<div class="caption"> <!-- 下部のキャプション領域(構造は固定) -->
<p class="caption-title">緑が広がる公園</p>
<!-- 編集OK:タイトル文言のみ差し替え可。クラス名は変更不可 -->
<p class="caption-text">自然と人が調和する、心地よい憩いの場所。</p>
<!-- 編集OK:説明テキストのみ差し替え可。改行は <br> を使用 -->
</div> <!-- /.caption -->
</a>
</div> <!-- /.gallery-card(1枚目のテンプレート) -->
<div class="gallery-card"> <!-- スライド2枚目 -->
<a href="https://example.com/link2" target="_blank" rel="noopener">
<img src="https://picsum.photos/seed/school/1200/675" alt="学校の写真" loading="lazy"> <!-- 2枚目以降はlazy読み込み -->
<div class="caption">
<p class="caption-title">明るい教室風景</p>
<p class="caption-text">子どもたちの笑顔と学びの声が響く、温かな空間。</p>
</div>
</a>
</div>
<div class="gallery-card"> <!-- スライド3枚目 -->
<a href="https://example.com/link3" target="_blank" rel="noopener">
<img src="https://picsum.photos/seed/safari/1200/675" alt="サファリの写真" loading="lazy">
<div class="caption">
<p class="caption-title">アフリカのサファリ</p>
<p class="caption-text">雄大な自然の中で息づく野生動物たちとの出会い。</p>
</div>
</a>
</div>
<div class="gallery-card"> <!-- スライド4枚目 -->
<a href="https://example.com/link4" target="_blank" rel="noopener">
<img src="https://picsum.photos/seed/expo/1200/675" alt="博覧会の写真" loading="lazy">
<div class="caption">
<p class="caption-title">賑わう博覧会</p>
<p class="caption-text">世界の技術と文化が集う、華やかな交流の場。</p>
</div>
</a>
</div>
<div class="gallery-card"> <!-- スライド5枚目 -->
<a href="https://example.com/link5" target="_blank" rel="noopener">
<img src="https://picsum.photos/seed/city/1200/675" alt="街並みの写真" loading="lazy">
<div class="caption">
<p class="caption-title">夕暮れの街並み</p>
<p class="caption-text">オレンジ色の光に包まれた、静かなストリート。</p>
</div>
</a>
</div>
</div> <!-- /.gallery-track (スライド一覧の終了) -->
<!-- ▼ 矢印ボタンとドットナビ -->
<button class="gallery-btn prev" aria-label="前へ">❮</button> <!-- 左矢印ボタン(前スライド) -->
<button class="gallery-btn next" aria-label="次へ">❯</button> <!-- 右矢印ボタン(次スライド) -->
<div class="dots"></div> <!-- ドットナビゲーション(JSが自動生成) -->
</div> <!-- /.gallery-wrap -->
</section> <!-- /.gallery-section (全体の終わり) -->
/* ============================================
Netflix風スライドギャラリー JavaScript(完全版・修正版)
--------------------------------------------
・無限ループスライド+自動再生+ドット連動
・スワイプ/キーボード対応
・スマホ中央寄せ調整あり
--------------------------------------------
※HTML側では id="gallery" を持つ .gallery-wrap が必須
============================================ */
(function(){ // 即時実行関数でグローバル汚染を防止
/* ▼ ギャラリー全体を取得 */
const wrap = document.getElementById('gallery'); // スライダー全体のラッパー(必須ID)
if(!wrap) return; // ラッパーが無い場合は処理を中断(安全対策)
/* ▼ 内部構造の主要要素を取得 */
const track = wrap.querySelector('.gallery-track'); // スライドを横並びに保持するトラック要素
const prevBtn = wrap.querySelector('.gallery-btn.prev'); // 「前へ」ボタン
const nextBtn = wrap.querySelector('.gallery-btn.next'); // 「次へ」ボタン
const dotsWrap= wrap.querySelector('.dots'); // ドット(ページネーション)を入れるコンテナ
/* ▼ 動作パラメータ設定 */
const intervalTime = 6000; // 自動スライドの間隔(ミリ秒)。6000=6秒
const transitionMs = 600; // トラック移動のトランジション時間(ミリ秒)
/* ▼ 状態管理用変数 */
let autoSlide; // setInterval のハンドルを保持(停止・再開に利用)
let index; // 現在の「実トラック上」のインデックス(複製込みの位置)
let originalCount; // 元のスライドの枚数(論理インデックス計算で使用)
/* =====================================================
初期スライド情報を取得し、ドットを生成
===================================================== */
const originals = Array.from(track.children); // 初期のスライド要素を配列化(複製前の子要素)
originalCount = originals.length; // 元スライド枚数を記録
// ドットエリア初期化(既存の子要素を空に)
dotsWrap.innerHTML = '';
// 各スライドに対応するドットを生成(クリックでそのスライドへ移動)
const dots = originals.map((_, i) => { // i は論理インデックス(0..n-1)
const b = document.createElement('button'); // 新規ボタン要素を作成
b.type = 'button'; // フォーム送信を防ぐため type を明示
b.className = 'dot' + (i === 0 ? ' active' : ''); // 最初のドットだけ active クラスを付与
b.setAttribute('aria-label', `スライド ${i + 1}`); // スクリーンリーダー向けラベル
b.dataset.index = i; // 対応する論理インデックスをdata属性に保持
b.addEventListener('click', () => { // ドットクリック時の挙動
stopAuto(); // 自動再生を停止(操作優先)
goTo(i, true); // i 番目の論理スライドへ移動(アニメーション有)
startAuto(); // 操作後に自動再生を再開
});
dotsWrap.appendChild(b); // 作成したドットをDOMに追加
return b; // dots 配列として返す(後で状態更新に利用)
});
/* =====================================================
スライドサイズ・位置関連
===================================================== */
// CSS の gap 値(スライド間の隙間)を動的に取得
const getGap = () => parseFloat(getComputedStyle(track).gap) || 0;
// スライド1枚ぶんの移動幅を算出(カード幅+gap)
const slideSize = () => {
const first = track.querySelector('.gallery-card'); // 先頭のカード要素を取得
if (!first) return 0; // 存在しない場合は0を返す(ガード)
const rect = first.getBoundingClientRect(); // レイアウト計測
return rect.width + getGap(); // 幅+隙間を合算して返す
};
/* =====================================================
無限ループ構造を作る(前後に複製を追加)
===================================================== */
const before = originals.map(el => el.cloneNode(true)); // 先頭側に配置する複製群
const after = originals.map(el => el.cloneNode(true)); // 末尾側に配置する複製群
// いったんトラックを空にして、[後複製][本体][前複製]の順で並べ直す
track.innerHTML = ''; // 子要素をリセット
after.forEach(n => track.appendChild(n)); // 末尾側の複製を先に追加
originals.forEach(n => track.appendChild(n)); // 続いて本体スライドを追加
before.forEach(n => track.appendChild(n)); // 最後に先頭側の複製を追加
// 実インデックスを「本体の先頭」に合わせる(=配列の中央付近から開始)
index = originalCount; // after(本体の前)をスキップした位置
/* =====================================================
スライド位置を変更する関数
===================================================== */
function applyTransform(noAnim = false) { // noAnim=true なら瞬時に位置を適用
const isMobile = window.innerWidth <= 768; // 768px以下をスマホとみなす
let offset = slideSize() * index; // index に応じた移動量を計算
// スマホ時はカードを画面中央に見せるため、左右の余白分を補正
if (isMobile) {
const cardWidth = track.querySelector('.gallery-card').getBoundingClientRect().width; // カード幅を取得
const containerWidth = wrap.getBoundingClientRect().width; // ギャラリー全体の幅を取得
const sideSpace = (containerWidth - cardWidth) / 2; // 左右の余白を計算
offset -= sideSpace; // オフセットを補正
}
// アニメーションを一時的に無効化(瞬間移動させたい時)
if (noAnim) track.style.transition = 'none';
// 計算したオフセットでトラックを移動
track.style.transform = `translateX(-${offset}px)`;
// 一時無効化した場合は、次フレームでトランジションを復帰
if (noAnim) {
void track.offsetWidth; // リフローでスタイル適用を確定
track.style.transition = `transform ${transitionMs}ms ease`; // 元の遷移に戻す
}
}
/* =====================================================
ドットの状態更新
===================================================== */
function updateDots() {
// 実インデックス(複製込み)から論理インデックス(0..originalCount-1)へ正規化
const logical = (index - originalCount + originalCount * 1000) % originalCount;
// すべてのドットから active を外し、該当のドットにだけ付与
dots.forEach((d, i) => d.classList.toggle('active', i === logical));
}
/* =====================================================
任意スライドに移動
===================================================== */
function goTo(logicalIndex, animate = true) { // logicalIndex は 0..originalCount-1
index = logicalIndex + originalCount; // 実インデックスに変換(中央ブロックへ)
applyTransform(!animate); // animate=trueなら遷移、falseなら瞬時
updateDots(); // ドットの状態を更新
}
/* =====================================================
次・前ボタン操作
===================================================== */
function moveNext() { // 次のスライドへ
index++; // 実インデックスを1つ進める
applyTransform(); // アニメーションで移動
updateDots(); // ドット更新
}
function movePrev() { // 前のスライドへ
index--; // 実インデックスを1つ戻す
applyTransform(); // アニメーションで移動
updateDots(); // ドット更新
}
/* =====================================================
無限ループ補正(トランジション終了時)
===================================================== */
track.addEventListener('transitionend', () => { // アニメーションが終わったら
// 右端(末尾複製のさらに先)に到達 → 本体ブロックへ巻き戻す
if (index >= originalCount * 2) {
index -= originalCount; // 実インデックスを中央ブロックに戻す
applyTransform(true); // 瞬時に位置補正(見た目は連続)
}
// 左端(先頭複製のさらに手前)に到達 → 本体ブロックへ巻き進める
else if (index < originalCount) {
index += originalCount; // 実インデックスを中央ブロックに戻す
applyTransform(true); // 瞬時に位置補正
}
});
/* =====================================================
自動スライド(再生・停止)
===================================================== */
function startAuto() { // 自動再生開始
stopAuto(); // 二重起動防止でいったん停止
autoSlide = setInterval(moveNext, intervalTime);// 一定間隔で次スライドへ
}
function stopAuto() { // 自動再生停止
if (autoSlide) clearInterval(autoSlide); // タイマーがあれば解除
}
/* =====================================================
スワイプ操作(スマホ向け)
===================================================== */
let startX = 0; // 指を置いたX座標
let swiping = false; // スワイプ中かどうかのフラグ
// タッチ開始(指を置いた瞬間)
track.addEventListener('touchstart', e => {
startX = e.touches[0].clientX; // 最初の指のX位置を記録
swiping = true; // スワイプ開始
stopAuto(); // 操作中は自動再生を止める
}, { passive: true }); // スクロール最適化
// タッチ終了(指を離した瞬間)
track.addEventListener('touchend', e => {
if (!swiping) return; // スワイプしていなければ何もしない
const endX = e.changedTouches[0].clientX; // 離した時のX位置
if (startX - endX > 50) moveNext(); // 左へ50px以上 → 次へ
if (endX - startX > 50) movePrev(); // 右へ50px以上 → 前へ
swiping = false; // スワイプ終了
startAuto(); // 操作後は自動再生を再開
});
/* =====================================================
キーボード操作(左右矢印で操作可能)
===================================================== */
wrap.tabIndex = 0; // ラッパーにフォーカス可能属性を付与
wrap.addEventListener('keydown', e => { // キー押下を監視
if (e.key === 'ArrowRight') { // →キーで次へ
e.preventDefault(); // 画面スクロール等を防止
stopAuto(); moveNext(); startAuto(); // 操作→自動再生再開
}
if (e.key === 'ArrowLeft') { // ←キーで前へ
e.preventDefault(); // 画面スクロール等を防止
stopAuto(); movePrev(); startAuto(); // 操作→自動再生再開
}
});
/* =====================================================
ボタンクリック操作
===================================================== */
nextBtn.addEventListener('click', () => { // 「次へ」クリック時
stopAuto(); moveNext(); startAuto(); // 自動再生を一旦停止→移動→再開
});
prevBtn.addEventListener('click', () => { // 「前へ」クリック時
stopAuto(); movePrev(); startAuto(); // 自動再生を一旦停止→移動→再開
});
/* =====================================================
リサイズ対応(位置補正)
===================================================== */
let resizeTimer; // リサイズの連打抑制用タイマー
window.addEventListener('resize', () => { // 画面サイズが変わったら
clearTimeout(resizeTimer); // 直前の予約をキャンセル
resizeTimer = setTimeout(() => applyTransform(true), 300); // 少し待ってから瞬時補正
});
/* =====================================================
初期表示設定
===================================================== */
applyTransform(true); // まずは瞬時に正しい位置へ
updateDots(); // ドットの初期状態(1個目をactive)に整える
startAuto(); // 自動スライドを開始
})(); // 即時実行関数終了:ここまでがギャラリーの制御コード
コピペで完結!ZOZOTOWN風スライドギャラリー(自動で動くサムネ表示)【カルーセル】
コメント