サムネ自動スクロールプレビュー
カードにカーソルを乗せると、サムネイルの中でページ全体がするすると下までスクロールして見せてくれます。ギャラリーサイトやポートフォリオ一覧で、クリック前に中身を伝える定番の魅せUIです。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk: テンプレート一覧。カーソルを乗せると中身を下までプレビュー -->
<div class="stage" aria-label="FlowDeskのテンプレート3枚">
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4D7CFE,#7AA0FF)">
<img src="https://picsum.photos/seed/flowdesk-tpl-1/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)"><img src="https://picsum.photos/seed/flowdesk-tpl-1a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#9B6BFF,#4D7CFE)"><img src="https://picsum.photos/seed/flowdesk-tpl-1b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4D7CFE"></div>
</div>
</div>
<span class="cap">DASHBOARD</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)">
<img src="https://picsum.photos/seed/flowdesk-tpl-2/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t short"></div><div class="bar t"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#4D7CFE,#9B6BFF)"><img src="https://picsum.photos/seed/flowdesk-tpl-2a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#7AA0FF)"><img src="https://picsum.photos/seed/flowdesk-tpl-2b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4ED1A1"></div>
</div>
</div>
<span class="cap">KANBAN</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#9B6BFF,#4D7CFE)">
<img src="https://picsum.photos/seed/flowdesk-tpl-3/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#4D7CFE,#4ED1A1)"><img src="https://picsum.photos/seed/flowdesk-tpl-3a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#7AA0FF,#9B6BFF)"><img src="https://picsum.photos/seed/flowdesk-tpl-3b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t short"></div><div class="bar t"></div><div class="bar t"></div>
<div class="foot" style="background:#9B6BFF"></div>
</div>
</div>
<span class="cap">REPORT</span>
</article>
</div>
CSS
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #eef3ff; }
.stage {
display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: 22px; width: 100%; min-height: 100vh; max-height: 100%; padding: 24px;
background: linear-gradient(180deg, #f5f8ff 0%, #e6eefb 100%);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.thumb {
position: relative; width: 280px; border-radius: 12px; background: #fff;
box-shadow: 0 6px 18px rgba(28,46,90,.12);
transition: transform .35s var(--ease-out-expo), box-shadow .35s var(--ease-out-expo);
}
.thumb:hover, .thumb.hover { transform: translateY(-4px); box-shadow: 0 16px 32px rgba(28,46,90,.22); }
.viewport { height: 180px; overflow: hidden; border-radius: 12px 12px 0 0; }
.page { display: flex; flex-direction: column; min-height: 720px; padding-bottom: 16px; background: #fff; transform: translateY(0); }
.thumb:hover .page, .thumb.hover .page { animation: scroll 3.7s linear 1 forwards; }
.hero { position: relative; height: 160px; overflow: hidden; }
.hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bar { margin: 10px 16px 0; height: 9px; border-radius: 5px; background: #dce6f7; }
.bar.title { height: 16px; width: 60%; background: #c2d2ee; margin-top: 16px; }
.bar.t { width: 88%; }
.bar.t.short { width: 54%; }
.imgs { display: flex; gap: 10px; margin: 14px 16px 4px; }
.ph { position: relative; flex: 1; height: 90px; border-radius: 8px; overflow: hidden; }
.ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.foot { height: 64px; margin: 16px 0 0; }
.cap {
position: absolute; left: 12px; bottom: 10px;
font-size: 11px; font-weight: 700; letter-spacing: .14em; color: #1b2a4a;
background: rgba(255,255,255,.85); padding: 3px 8px; border-radius: 6px;
}
@keyframes scroll {
0% { transform: translateY(0); animation-timing-function: linear; }
75.7% { transform: translateY(-540px); animation-timing-function: linear; }
86.5% { transform: translateY(-540px); animation-timing-function: var(--ease-inout); }
100% { transform: translateY(0); }
}
JavaScript
// hoverはCSS。疑似hoverを順番に回し、画像の読み込み失敗を補う(デモと同じロジック)。
(() => {
const cards = [...document.querySelectorAll('[data-thumb]')];
if (!cards.length) return;
document.querySelectorAll('img').forEach(img => {
img.addEventListener('error', () => { img.style.display = 'none'; });
});
let i = 0;
const cycle = () => {
const card = cards[i % cards.length];
card.classList.add('hover');
setTimeout(() => card.classList.remove('hover'), 3700);
i++;
};
cycle();
setInterval(cycle, 4200);
})();
実装ガイド
使いどころ
ギャラリーやポートフォリオ、テンプレート一覧で、クリック前に中身を伝えたいときに。サムネ内でページ全体を見せる定番の魅せUIです。
実装時の注意点
縦長スクショは用意せず、カード内にCSSで疑似ページ(hero/見出しバー/画像/フッター)を組み、hoverで内部をtranslateYスクロールします。スクロール区間は必ずlinear、戻りだけイージング。疑似hoverを巡回させ、実hoverでも同じクラスで動かします。画像はonerrorでグラデにフォールバック。
対応ブラウザ
CSS animation/transform・overflow:hiddenは全モダンブラウザで安定動作します。hover前提のためタッチではタップやアニメ自動再生にフォールバックを入れ、対応は実機で確認してください。
よくある失敗
スクロール部にlinear以外を使うと等速感が消えます。ページ高さが窓+移動量より低いとフッターまで出ません。画像未読込のチラつきはグラデ下地で回避します。
応用例
疑似ページを実スクショ画像に差し替える、hoverズームや影で奥行き、クリックで詳細へ遷移、ニュース一覧のプレビューなどに発展できます。
コード
HTML
<!-- サムネ自動スクロールプレビュー:hoverでサムネ内のページが下までスクロール -->
<div class="stage" aria-label="自動スクロールするサムネイル3枚">
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4D7CFE,#9B6BFF)">
<img src="https://picsum.photos/seed/wedelab-thumb-1/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)"><img src="https://picsum.photos/seed/wedelab-thumb-1a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#FFC83D,#FF5C5C)"><img src="https://picsum.photos/seed/wedelab-thumb-1b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4D7CFE"></div>
</div>
</div>
<span class="cap">PROJECT 01</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)">
<img src="https://picsum.photos/seed/wedelab-thumb-2/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t short"></div><div class="bar t"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#9B6BFF,#4D7CFE)"><img src="https://picsum.photos/seed/wedelab-thumb-2a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#FFC83D)"><img src="https://picsum.photos/seed/wedelab-thumb-2b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4ED1A1"></div>
</div>
</div>
<span class="cap">PROJECT 02</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#FF5C5C,#FFC83D)">
<img src="https://picsum.photos/seed/wedelab-thumb-3/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#FF5C5C,#9B6BFF)"><img src="https://picsum.photos/seed/wedelab-thumb-3a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#FFC83D,#4ED1A1)"><img src="https://picsum.photos/seed/wedelab-thumb-3b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t short"></div><div class="bar t"></div><div class="bar t"></div>
<div class="foot" style="background:#FF5C5C"></div>
</div>
</div>
<span class="cap">PROJECT 03</span>
</article>
</div>
CSS
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }
.stage {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 22px;
width: 100%;
min-height: 100vh;
max-height: 100%;
padding: 24px;
background: #F7F5F0;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.thumb {
position: relative;
width: 280px;
border-radius: 12px;
background: #fff;
box-shadow: 0 6px 18px rgba(0,0,0,.12);
transition: transform .35s var(--ease-out-expo), box-shadow .35s var(--ease-out-expo);
}
.thumb:hover, .thumb.hover {
transform: translateY(-4px);
box-shadow: 0 16px 32px rgba(0,0,0,.2);
}
/* 覗き窓(180px)。中の page がここをスクロールする */
.viewport {
height: 180px;
overflow: hidden;
border-radius: 12px 12px 0 0;
}
.page {
display: flex;
flex-direction: column;
min-height: 720px;
padding-bottom: 16px;
background: #fff;
transform: translateY(0);
}
/* hover中だけ自動スクロール。スクロール区間は必ず linear */
.thumb:hover .page,
.thumb.hover .page { animation: scroll 3.7s linear 1 forwards; }
.hero { position: relative; height: 160px; overflow: hidden; }
.hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bar { margin: 10px 16px 0; height: 9px; border-radius: 5px; background: #E3E3E0; }
.bar.title { height: 16px; width: 60%; background: #C9C9C4; margin-top: 16px; }
.bar.t { width: 88%; }
.bar.t.short { width: 54%; }
.imgs { display: flex; gap: 10px; margin: 14px 16px 4px; }
.ph { position: relative; flex: 1; height: 90px; border-radius: 8px; overflow: hidden; }
.ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.foot { height: 64px; margin: 16px 0 0; }
.cap {
position: absolute; left: 12px; bottom: 10px;
font-size: 11px; font-weight: 700; letter-spacing: .14em;
color: #1A1A1A;
background: rgba(255,255,255,.82);
padding: 3px 8px; border-radius: 6px;
}
@keyframes scroll {
0% { transform: translateY(0); animation-timing-function: linear; }
75.7% { transform: translateY(-540px); animation-timing-function: linear; } /* 2.8s で最下部へ */
86.5% { transform: translateY(-540px); animation-timing-function: var(--ease-inout); } /* 0.4s 停止 */
100% { transform: translateY(0); } /* 0.5s で先頭へ */
}
JavaScript
// カードへのhoverはCSSが担当。ここでは疑似hoverを順番に回し、画像の読み込み失敗を補う。
(() => {
const cards = [...document.querySelectorAll('[data-thumb]')];
if (!cards.length) return; // null安全
// 画像が読めなければ隠して親のグラデを見せる(オフライン/失敗でもデモ成立)
document.querySelectorAll('img').forEach(img => {
img.addEventListener('error', () => { img.style.display = 'none'; });
});
// 4.2sごとに次の1枚へ .hover を付与(3.7sで除去)。実hoverと同じ動き。
let i = 0;
const cycle = () => {
const card = cards[i % cards.length];
card.classList.add('hover');
setTimeout(() => card.classList.remove('hover'), 3700);
i++;
};
cycle();
setInterval(cycle, 4200);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「サムネ自動スクロールプレビュー」の効果を追加してください。
# 追加してほしい効果
サムネ自動スクロールプレビュー(UIコンポーネント)
カードにカーソルを乗せると、サムネイルの中でページ全体がするすると下までスクロールして見せてくれます。ギャラリーサイトやポートフォリオ一覧で、クリック前に中身を伝える定番の魅せUIです。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- サムネ自動スクロールプレビュー:hoverでサムネ内のページが下までスクロール -->
<div class="stage" aria-label="自動スクロールするサムネイル3枚">
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4D7CFE,#9B6BFF)">
<img src="https://picsum.photos/seed/wedelab-thumb-1/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)"><img src="https://picsum.photos/seed/wedelab-thumb-1a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#FFC83D,#FF5C5C)"><img src="https://picsum.photos/seed/wedelab-thumb-1b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4D7CFE"></div>
</div>
</div>
<span class="cap">PROJECT 01</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#4ED1A1,#4D7CFE)">
<img src="https://picsum.photos/seed/wedelab-thumb-2/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t short"></div><div class="bar t"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#9B6BFF,#4D7CFE)"><img src="https://picsum.photos/seed/wedelab-thumb-2a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#4ED1A1,#FFC83D)"><img src="https://picsum.photos/seed/wedelab-thumb-2b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="foot" style="background:#4ED1A1"></div>
</div>
</div>
<span class="cap">PROJECT 02</span>
</article>
<article class="thumb" data-thumb>
<div class="viewport">
<div class="page">
<div class="hero" style="background:linear-gradient(135deg,#FF5C5C,#FFC83D)">
<img src="https://picsum.photos/seed/wedelab-thumb-3/560/320" alt="" loading="lazy">
</div>
<div class="bar title"></div>
<div class="bar t"></div><div class="bar t"></div><div class="bar t short"></div>
<div class="imgs">
<div class="ph" style="background:linear-gradient(135deg,#FF5C5C,#9B6BFF)"><img src="https://picsum.photos/seed/wedelab-thumb-3a/260/180" alt="" loading="lazy"></div>
<div class="ph" style="background:linear-gradient(135deg,#FFC83D,#4ED1A1)"><img src="https://picsum.photos/seed/wedelab-thumb-3b/260/180" alt="" loading="lazy"></div>
</div>
<div class="bar t short"></div><div class="bar t"></div><div class="bar t"></div>
<div class="foot" style="background:#FF5C5C"></div>
</div>
</div>
<span class="cap">PROJECT 03</span>
</article>
</div>
【CSS】
:root{
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
}
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; min-height: 100%; background: #F7F5F0; }
.stage {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 22px;
width: 100%;
min-height: 100vh;
max-height: 100%;
padding: 24px;
background: #F7F5F0;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.thumb {
position: relative;
width: 280px;
border-radius: 12px;
background: #fff;
box-shadow: 0 6px 18px rgba(0,0,0,.12);
transition: transform .35s var(--ease-out-expo), box-shadow .35s var(--ease-out-expo);
}
.thumb:hover, .thumb.hover {
transform: translateY(-4px);
box-shadow: 0 16px 32px rgba(0,0,0,.2);
}
/* 覗き窓(180px)。中の page がここをスクロールする */
.viewport {
height: 180px;
overflow: hidden;
border-radius: 12px 12px 0 0;
}
.page {
display: flex;
flex-direction: column;
min-height: 720px;
padding-bottom: 16px;
background: #fff;
transform: translateY(0);
}
/* hover中だけ自動スクロール。スクロール区間は必ず linear */
.thumb:hover .page,
.thumb.hover .page { animation: scroll 3.7s linear 1 forwards; }
.hero { position: relative; height: 160px; overflow: hidden; }
.hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bar { margin: 10px 16px 0; height: 9px; border-radius: 5px; background: #E3E3E0; }
.bar.title { height: 16px; width: 60%; background: #C9C9C4; margin-top: 16px; }
.bar.t { width: 88%; }
.bar.t.short { width: 54%; }
.imgs { display: flex; gap: 10px; margin: 14px 16px 4px; }
.ph { position: relative; flex: 1; height: 90px; border-radius: 8px; overflow: hidden; }
.ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.foot { height: 64px; margin: 16px 0 0; }
.cap {
position: absolute; left: 12px; bottom: 10px;
font-size: 11px; font-weight: 700; letter-spacing: .14em;
color: #1A1A1A;
background: rgba(255,255,255,.82);
padding: 3px 8px; border-radius: 6px;
}
@keyframes scroll {
0% { transform: translateY(0); animation-timing-function: linear; }
75.7% { transform: translateY(-540px); animation-timing-function: linear; } /* 2.8s で最下部へ */
86.5% { transform: translateY(-540px); animation-timing-function: var(--ease-inout); } /* 0.4s 停止 */
100% { transform: translateY(0); } /* 0.5s で先頭へ */
}
【JavaScript】
// カードへのhoverはCSSが担当。ここでは疑似hoverを順番に回し、画像の読み込み失敗を補う。
(() => {
const cards = [...document.querySelectorAll('[data-thumb]')];
if (!cards.length) return; // null安全
// 画像が読めなければ隠して親のグラデを見せる(オフライン/失敗でもデモ成立)
document.querySelectorAll('img').forEach(img => {
img.addEventListener('error', () => { img.style.display = 'none'; });
});
// 4.2sごとに次の1枚へ .hover を付与(3.7sで除去)。実hoverと同じ動き。
let i = 0;
const cycle = () => {
const card = cards[i % cards.length];
card.classList.add('hover');
setTimeout(() => card.classList.remove('hover'), 3700);
i++;
};
cycle();
setInterval(cycle, 4200);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。