サムネ付きカルーセル

大きなメイン画像と下段のサムネイル列を組み合わせたカルーセル。サムネや矢印キーで切り替わり、現在位置のサムネだけが縁取りで強調される作品紹介向けUIです。

#css#js#gallery

ライブデモ

コード

HTML
<!-- サムネ付きカルーセル: 大きなメイン画像+下のサムネ列。クリック/矢印キーで切替、現在位置を強調 -->
<div class="crs" aria-roledescription="カルーセル" aria-label="作品カルーセル">
  <div class="crs__stage" id="crsStage">
    <button class="crs__arrow crs__arrow--prev" type="button" aria-label="前のスライド">‹</button>

    <div class="crs__slide is-active" data-index="0">
      <img src="https://picsum.photos/id/1015/900/600" alt="断崖から見下ろすフィヨルド">
    </div>
    <div class="crs__slide" data-index="1">
      <img src="https://picsum.photos/id/1025/900/600" alt="毛布にくるまる犬">
    </div>
    <div class="crs__slide" data-index="2">
      <img src="https://picsum.photos/id/1039/900/600" alt="緑の渓谷に流れる滝">
    </div>
    <div class="crs__slide" data-index="3">
      <img src="https://picsum.photos/id/1043/900/600" alt="渓谷の森と静かな水面">
    </div>
    <div class="crs__slide" data-index="4">
      <img src="https://picsum.photos/id/1035/900/600" alt="虹のかかる渓谷の滝">
    </div>
    <div class="crs__slide" data-index="5">
      <img src="https://picsum.photos/id/1074/900/600" alt="野生のライオンの横顔">
    </div>

    <button class="crs__arrow crs__arrow--next" type="button" aria-label="次のスライド">›</button>
    <p class="crs__caption" id="crsCaption" aria-live="polite">断崖から見下ろすフィヨルド</p>
  </div>

  <ul class="crs__thumbs" id="crsThumbs" role="tablist" aria-label="サムネイル一覧">
    <li role="presentation">
      <button class="crs__thumb is-current" type="button" role="tab" aria-selected="true" aria-label="1枚目: 断崖から見下ろすフィヨルド" data-index="0">
        <img src="https://picsum.photos/id/1015/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="2枚目: 毛布にくるまる犬" data-index="1">
        <img src="https://picsum.photos/id/1025/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="3枚目: 緑の渓谷に流れる滝" data-index="2">
        <img src="https://picsum.photos/id/1039/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="4枚目: 渓谷の森と静かな水面" data-index="3">
        <img src="https://picsum.photos/id/1043/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="5枚目: 虹のかかる渓谷の滝" data-index="4">
        <img src="https://picsum.photos/id/1035/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="6枚目: 野生のライオンの横顔" data-index="5">
        <img src="https://picsum.photos/id/1074/160/110" alt="" loading="lazy">
      </button>
    </li>
  </ul>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 0%, #1c2440, #0b0e1c 75%);
  font-family: "Segoe UI", system-ui, sans-serif;
  padding: 20px;
}

.crs { width: min(92vw, 480px); display: flex; flex-direction: column; gap: 12px; }

.crs__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: 16px;
  overflow: hidden;
  background: #10142a;
  box-shadow: 0 24px 50px -20px rgba(0, 0, 0, .7);
}

.crs__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity .55s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
  pointer-events: none;
}
.crs__slide.is-active { opacity: 1; transform: scale(1); pointer-events: auto; z-index: 1; }
.crs__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

.crs__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 0;
  background: rgba(10, 12, 22, .55);
  color: #fff;
  font-size: 18px;
  cursor: pointer;
  display: grid;
  place-items: center;
  backdrop-filter: blur(3px);
  transition: background .2s ease, transform .2s ease;
}
.crs__arrow:hover { background: rgba(10, 12, 22, .8); }
.crs__arrow:active { transform: translateY(-50%) scale(.92); }
.crs__arrow:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }
.crs__arrow--prev { left: 10px; }
.crs__arrow--next { right: 10px; }

.crs__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  padding: 22px 46px 10px;
  font-size: 12.5px;
  letter-spacing: .03em;
  color: #eef0fa;
  text-align: center;
  background: linear-gradient(to top, rgba(6, 7, 16, .8), rgba(6, 7, 16, 0));
}

.crs__thumbs {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
}
.crs__thumb {
  position: relative;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  overflow: hidden;
  aspect-ratio: 16 / 11;
  cursor: pointer;
  background: #10142a;
  opacity: .55;
  transition: opacity .25s ease, border-color .25s ease, transform .2s ease;
}
.crs__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.crs__thumb:hover { opacity: .85; transform: translateY(-2px); }
.crs__thumb.is-current {
  opacity: 1;
  border-color: #7fa3ff;
  box-shadow: 0 0 0 2px rgba(127, 163, 255, .25);
}
.crs__thumb:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }

@media (max-width: 420px) {
  .crs__thumbs { grid-template-columns: repeat(3, 1fr); }
}
JavaScript
// カルーセル: サムネクリック/矢印ボタン/矢印キーでメイン画像をクロスフェード切替し、現在位置を強調する
(() => {
  const stage = document.getElementById("crsStage");
  const thumbsWrap = document.getElementById("crsThumbs");
  const caption = document.getElementById("crsCaption");
  if (!stage || !thumbsWrap) return; // null安全

  const slides = Array.from(stage.querySelectorAll(".crs__slide"));
  const thumbs = Array.from(thumbsWrap.querySelectorAll(".crs__thumb"));
  const prevBtn = stage.querySelector(".crs__arrow--prev");
  const nextBtn = stage.querySelector(".crs__arrow--next");
  if (!slides.length || !thumbs.length) return;

  let current = 0;

  const goTo = (index) => {
    current = (index + slides.length) % slides.length;
    slides.forEach((s, i) => s.classList.toggle("is-active", i === current));
    thumbs.forEach((t, i) => {
      const active = i === current;
      t.classList.toggle("is-current", active);
      t.setAttribute("aria-selected", String(active));
    });
    if (caption) {
      const img = slides[current] && slides[current].querySelector("img");
      caption.textContent = img ? img.getAttribute("alt") || "" : "";
    }
  };

  thumbs.forEach((btn, i) => btn.addEventListener("click", () => goTo(i)));
  if (prevBtn) prevBtn.addEventListener("click", () => goTo(current - 1));
  if (nextBtn) nextBtn.addEventListener("click", () => goTo(current + 1));

  // カルーセル領域にフォーカスがある間は左右矢印キーでも切替できる
  const root = stage.closest(".crs") || stage;
  root.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight") { e.preventDefault(); goTo(current + 1); }
    else if (e.key === "ArrowLeft") { e.preventDefault(); goTo(current - 1); }
  });

  goTo(0); // 初期状態を明示的に反映
})();

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

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

# 追加してほしい効果
サムネ付きカルーセル(画像 & ギャラリー)
大きなメイン画像と下段のサムネイル列を組み合わせたカルーセル。サムネや矢印キーで切り替わり、現在位置のサムネだけが縁取りで強調される作品紹介向けUIです。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- サムネ付きカルーセル: 大きなメイン画像+下のサムネ列。クリック/矢印キーで切替、現在位置を強調 -->
<div class="crs" aria-roledescription="カルーセル" aria-label="作品カルーセル">
  <div class="crs__stage" id="crsStage">
    <button class="crs__arrow crs__arrow--prev" type="button" aria-label="前のスライド">‹</button>

    <div class="crs__slide is-active" data-index="0">
      <img src="https://picsum.photos/id/1015/900/600" alt="断崖から見下ろすフィヨルド">
    </div>
    <div class="crs__slide" data-index="1">
      <img src="https://picsum.photos/id/1025/900/600" alt="毛布にくるまる犬">
    </div>
    <div class="crs__slide" data-index="2">
      <img src="https://picsum.photos/id/1039/900/600" alt="緑の渓谷に流れる滝">
    </div>
    <div class="crs__slide" data-index="3">
      <img src="https://picsum.photos/id/1043/900/600" alt="渓谷の森と静かな水面">
    </div>
    <div class="crs__slide" data-index="4">
      <img src="https://picsum.photos/id/1035/900/600" alt="虹のかかる渓谷の滝">
    </div>
    <div class="crs__slide" data-index="5">
      <img src="https://picsum.photos/id/1074/900/600" alt="野生のライオンの横顔">
    </div>

    <button class="crs__arrow crs__arrow--next" type="button" aria-label="次のスライド">›</button>
    <p class="crs__caption" id="crsCaption" aria-live="polite">断崖から見下ろすフィヨルド</p>
  </div>

  <ul class="crs__thumbs" id="crsThumbs" role="tablist" aria-label="サムネイル一覧">
    <li role="presentation">
      <button class="crs__thumb is-current" type="button" role="tab" aria-selected="true" aria-label="1枚目: 断崖から見下ろすフィヨルド" data-index="0">
        <img src="https://picsum.photos/id/1015/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="2枚目: 毛布にくるまる犬" data-index="1">
        <img src="https://picsum.photos/id/1025/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="3枚目: 緑の渓谷に流れる滝" data-index="2">
        <img src="https://picsum.photos/id/1039/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="4枚目: 渓谷の森と静かな水面" data-index="3">
        <img src="https://picsum.photos/id/1043/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="5枚目: 虹のかかる渓谷の滝" data-index="4">
        <img src="https://picsum.photos/id/1035/160/110" alt="" loading="lazy">
      </button>
    </li>
    <li role="presentation">
      <button class="crs__thumb" type="button" role="tab" aria-selected="false" aria-label="6枚目: 野生のライオンの横顔" data-index="5">
        <img src="https://picsum.photos/id/1074/160/110" alt="" loading="lazy">
      </button>
    </li>
  </ul>
</div>

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

.crs { width: min(92vw, 480px); display: flex; flex-direction: column; gap: 12px; }

.crs__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: 16px;
  overflow: hidden;
  background: #10142a;
  box-shadow: 0 24px 50px -20px rgba(0, 0, 0, .7);
}

.crs__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity .55s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
  pointer-events: none;
}
.crs__slide.is-active { opacity: 1; transform: scale(1); pointer-events: auto; z-index: 1; }
.crs__slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

.crs__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 0;
  background: rgba(10, 12, 22, .55);
  color: #fff;
  font-size: 18px;
  cursor: pointer;
  display: grid;
  place-items: center;
  backdrop-filter: blur(3px);
  transition: background .2s ease, transform .2s ease;
}
.crs__arrow:hover { background: rgba(10, 12, 22, .8); }
.crs__arrow:active { transform: translateY(-50%) scale(.92); }
.crs__arrow:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }
.crs__arrow--prev { left: 10px; }
.crs__arrow--next { right: 10px; }

.crs__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  padding: 22px 46px 10px;
  font-size: 12.5px;
  letter-spacing: .03em;
  color: #eef0fa;
  text-align: center;
  background: linear-gradient(to top, rgba(6, 7, 16, .8), rgba(6, 7, 16, 0));
}

.crs__thumbs {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
}
.crs__thumb {
  position: relative;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 8px;
  overflow: hidden;
  aspect-ratio: 16 / 11;
  cursor: pointer;
  background: #10142a;
  opacity: .55;
  transition: opacity .25s ease, border-color .25s ease, transform .2s ease;
}
.crs__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.crs__thumb:hover { opacity: .85; transform: translateY(-2px); }
.crs__thumb.is-current {
  opacity: 1;
  border-color: #7fa3ff;
  box-shadow: 0 0 0 2px rgba(127, 163, 255, .25);
}
.crs__thumb:focus-visible { outline: 2px solid #8ab4ff; outline-offset: 2px; }

@media (max-width: 420px) {
  .crs__thumbs { grid-template-columns: repeat(3, 1fr); }
}

【JavaScript】
// カルーセル: サムネクリック/矢印ボタン/矢印キーでメイン画像をクロスフェード切替し、現在位置を強調する
(() => {
  const stage = document.getElementById("crsStage");
  const thumbsWrap = document.getElementById("crsThumbs");
  const caption = document.getElementById("crsCaption");
  if (!stage || !thumbsWrap) return; // null安全

  const slides = Array.from(stage.querySelectorAll(".crs__slide"));
  const thumbs = Array.from(thumbsWrap.querySelectorAll(".crs__thumb"));
  const prevBtn = stage.querySelector(".crs__arrow--prev");
  const nextBtn = stage.querySelector(".crs__arrow--next");
  if (!slides.length || !thumbs.length) return;

  let current = 0;

  const goTo = (index) => {
    current = (index + slides.length) % slides.length;
    slides.forEach((s, i) => s.classList.toggle("is-active", i === current));
    thumbs.forEach((t, i) => {
      const active = i === current;
      t.classList.toggle("is-current", active);
      t.setAttribute("aria-selected", String(active));
    });
    if (caption) {
      const img = slides[current] && slides[current].querySelector("img");
      caption.textContent = img ? img.getAttribute("alt") || "" : "";
    }
  };

  thumbs.forEach((btn, i) => btn.addEventListener("click", () => goTo(i)));
  if (prevBtn) prevBtn.addEventListener("click", () => goTo(current - 1));
  if (nextBtn) nextBtn.addEventListener("click", () => goTo(current + 1));

  // カルーセル領域にフォーカスがある間は左右矢印キーでも切替できる
  const root = stage.closest(".crs") || stage;
  root.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight") { e.preventDefault(); goTo(current + 1); }
    else if (e.key === "ArrowLeft") { e.preventDefault(); goTo(current - 1); }
  });

  goTo(0); // 初期状態を明示的に反映
})();

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

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