サムネ自動スクロールプレビュー

カードにカーソルを乗せると、サムネイルの中でページ全体がするすると下までスクロールして見せてくれます。ギャラリーサイトやポートフォリオ一覧で、クリック前に中身を伝える定番の魅せUIです。

#thumbnail#preview#auto-scroll#card

ライブデモ

使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。