サムネ付きカルーセル
大きなメイン画像と下段のサムネイル列を組み合わせたカルーセル。サムネや矢印キーで切り替わり、現在位置のサムネだけが縁取りで強調される作品紹介向け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); // 初期状態を明示的に反映
})();
🤖 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。