Sticky スタックスクロール

position: sticky と top のずらしで、スクロールするとカードが重なり積み上がるストーリーテリング向けレイアウト。進捗バーをJSで連動。

#css#sticky#scroll

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura ライブツアー物語:sticky スタックでスクロール演出 -->
<div class="sk-ss">
  <p class="sk-ss__hint">↓ スクロールでツアーの軌跡をたどる</p>
  <div class="sk-ss__scroll" id="skSsScroll">
    <section class="sk-scard sk-scard--1">
      <span class="sk-scard__no">DAY 01</span>
      <div class="sk-scard__thumb" style="background-image:url('https://picsum.photos/300/200?random=71')"></div>
      <div class="sk-scard__body">
        <h2>東京 開幕</h2>
        <p>満開の桜とともにツアー「春一会」開幕。5人の笑顔がアリーナを包みました。</p>
      </div>
    </section>
    <section class="sk-scard sk-scard--2">
      <span class="sk-scard__no">DAY 02</span>
      <div class="sk-scard__thumb" style="background-image:url('https://picsum.photos/300/200?random=72')"></div>
      <div class="sk-scard__body">
        <h2>大阪 城ホール</h2>
        <p>新曲「ペタル・ダンス」を初披露。ピンクのペンライトが客席を染めました。</p>
      </div>
    </section>
    <section class="sk-scard sk-scard--3">
      <span class="sk-scard__no">DAY 03</span>
      <div class="sk-scard__thumb" style="background-image:url('https://picsum.photos/300/200?random=73')"></div>
      <div class="sk-scard__body">
        <h2>名古屋 夜公演</h2>
        <p>ことねのソロパートに大歓声。アンコールは全員で「桜ロード」を熱唱。</p>
      </div>
    </section>
    <section class="sk-scard sk-scard--4">
      <span class="sk-scard__no">FINAL</span>
      <div class="sk-scard__thumb" style="background-image:url('https://picsum.photos/300/200?random=74')"></div>
      <div class="sk-scard__body">
        <h2>福岡 ファイナル</h2>
        <p>「また来年、満開の季節に。」次のツアー開催も電撃発表されました。</p>
      </div>
    </section>
  </div>
  <div class="sk-ss__progress" aria-hidden="true"><i id="skSsBar"></i></div>
</div>
CSS
/* Sakura:position: sticky でライブツアーのストーリーカードを積み上げる */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff8fb3;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  background: linear-gradient(165deg, #fff0f6, #ffe3ee);
  color: #6a2740;
  height: 400px;
  overflow: hidden;
}

.sk-ss { position: relative; height: 400px; display: flex; flex-direction: column; }
.sk-ss__hint {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  font-size: 11px;
  color: #b05a7e;
  pointer-events: none;
  background: rgba(255, 255, 255, 0.8);
  padding: 4px 12px;
  border-radius: 999px;
  box-shadow: 0 2px 8px rgba(214, 94, 140, 0.18);
}

/* スクロール領域 */
.sk-ss__scroll {
  flex: 1;
  overflow-y: auto;
  scroll-behavior: smooth;
  padding: 40px 16px 260px; /* 末尾余白でスタックを成立させる */
  display: flex;
  flex-direction: column;
  gap: 20px;
  align-items: center;
}
.sk-ss__scroll::-webkit-scrollbar { width: 7px; }
.sk-ss__scroll::-webkit-scrollbar-thumb { background: var(--pink-deep); border-radius: 8px; }

/* スタックカード:sticky で上端に貼り付き、次が覆い被さる */
.sk-scard {
  position: sticky;
  top: 20px;
  width: min(440px, 100%);
  min-height: 130px;
  border-radius: 18px;
  padding: 14px;
  display: flex;
  gap: 14px;
  align-items: center;
  box-shadow: 0 18px 36px -18px rgba(214, 94, 140, 0.6);
  background: #fff;
  border: 1px solid #ffd9e6;
}
.sk-scard__no {
  position: absolute;
  top: 12px;
  right: 14px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.18em;
  color: var(--pink-deep);
}
.sk-scard__thumb {
  flex: 0 0 110px;
  height: 100px;
  border-radius: 12px;
  background-size: cover;
  background-position: center;
}
.sk-scard__body { flex: 1; }
.sk-scard h2 { font-size: 18px; font-weight: 800; margin-bottom: 6px; }
.sk-scard p { font-size: 12px; line-height: 1.7; color: #8a4a64; }

/* top をずらして重なりの段差を出す+各カードに桜色のアクセント */
.sk-scard--1 { top: 20px; border-top: 4px solid #ffb3cd; }
.sk-scard--2 { top: 32px; border-top: 4px solid #ff9cc0; }
.sk-scard--3 { top: 44px; border-top: 4px solid #ff85b3; }
.sk-scard--4 { top: 56px; border-top: 4px solid #ff6fa6; }

/* スクロール進捗バー */
.sk-ss__progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 5px;
  background: #ffe0ea;
}
.sk-ss__progress i {
  display: block;
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #ffb3cd, #ff6fa6);
  transition: width 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .sk-ss__scroll { scroll-behavior: auto; }
  .sk-ss__progress i { transition: none; }
}
JavaScript
// スクロール量に応じてツアー進捗バーを更新(null安全・受動リスナ)
(() => {
  const scroller = document.getElementById("skSsScroll");
  const bar = document.getElementById("skSsBar");
  if (!scroller || !bar) return;

  const update = () => {
    const max = scroller.scrollHeight - scroller.clientHeight;
    const ratio = max > 0 ? scroller.scrollTop / max : 0;
    bar.style.width = Math.min(100, ratio * 100).toFixed(1) + "%";
  };
  scroller.addEventListener("scroll", update, { passive: true });
  update(); // 初期表示
})();

コード

HTML
<!-- position: sticky スタック:スクロールでカードが重なり積み上がるレイアウト -->
<div class="ss">
  <p class="ss__hint">↓ スクロールしてカードを重ねる</p>
  <div class="ss__scroll" id="ssScroll">
    <section class="scard scard--1">
      <span class="scard__no">01</span>
      <h2>Sticky で重ねる</h2>
      <p>各カードに top を指定すると、上端で止まり次が覆い被さります。</p>
    </section>
    <section class="scard scard--2">
      <span class="scard__no">02</span>
      <h2>奥行きの演出</h2>
      <p>少しずつ縮小・回転させると立体的なスタックに見えます。</p>
    </section>
    <section class="scard scard--3">
      <span class="scard__no">03</span>
      <h2>JS不要が基本</h2>
      <p>進捗バーだけ JS で連動。レイアウト自体は CSS sticky のみ。</p>
    </section>
    <section class="scard scard--4">
      <span class="scard__no">04</span>
      <h2>最後のカード</h2>
      <p>ストーリーテリングや LP のヒーロー連結に最適です。</p>
    </section>
  </div>
  <div class="ss__progress" aria-hidden="true"><i id="ssBar"></i></div>
</div>
CSS
/* 全体:暗い背景にカラフルなスタックカード */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: "Hiragino Sans", system-ui, sans-serif;
  background: #0d1018; color: #f8fafc; height: 100vh; overflow: hidden;
}
.ss { position: relative; height: 100vh; display: flex; flex-direction: column; }
.ss__hint {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
  z-index: 5; font-size: 12px; color: #94a3b8; pointer-events: none;
  background: rgba(13,16,24,.7); padding: 4px 12px; border-radius: 20px;
}

/* スクロール領域 */
.ss__scroll {
  flex: 1; overflow-y: auto; scroll-behavior: smooth;
  padding: 48px 16px 60vh; /* 末尾に余白を作りスタックを成立させる */
  display: flex; flex-direction: column; gap: 26px; align-items: center;
}
.ss__scroll::-webkit-scrollbar { width: 8px; }
.ss__scroll::-webkit-scrollbar-thumb { background: #334155; border-radius: 8px; }

/* スタックカード:sticky で上端に貼り付く */
.scard {
  position: sticky; top: 24px;
  width: min(560px, 100%); min-height: 180px;
  border-radius: 18px; padding: 22px 24px;
  display: flex; flex-direction: column; gap: 8px; justify-content: center;
  box-shadow: 0 24px 50px -24px rgba(0,0,0,.7);
  color: #0b1020;
}
.scard__no { font-size: 12px; font-weight: 800; letter-spacing: .2em; opacity: .65; }
.scard h2 { font-size: clamp(20px, 4vw, 28px); font-weight: 800; }
.scard p { font-size: 14px; line-height: 1.7; max-width: 44ch; opacity: .9; }

/* 重なり時の段差を出すため top をずらす */
.scard--1 { top: 24px;  background: linear-gradient(135deg, #fda4af, #fb7185); }
.scard--2 { top: 38px;  background: linear-gradient(135deg, #a5b4fc, #818cf8); color: #f8fafc; }
.scard--3 { top: 52px;  background: linear-gradient(135deg, #6ee7b7, #34d399); }
.scard--4 { top: 66px;  background: linear-gradient(135deg, #fcd34d, #fbbf24); }

/* スクロール進捗バー */
.ss__progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 5px;
  background: #1e293b;
}
.ss__progress i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, #fb7185, #818cf8, #34d399);
  transition: width .1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .ss__scroll { scroll-behavior: auto; }
  .ss__progress i { transition: none; }
}
JavaScript
// スクロール量に応じて進捗バーを更新(null安全・受動リスナで滑らかに)
(() => {
  const scroller = document.getElementById('ssScroll');
  const bar = document.getElementById('ssBar');
  if (!scroller || !bar) return;

  const update = () => {
    const max = scroller.scrollHeight - scroller.clientHeight;
    const ratio = max > 0 ? scroller.scrollTop / max : 0;
    bar.style.width = Math.min(100, ratio * 100).toFixed(1) + '%';
  };
  scroller.addEventListener('scroll', update, { passive: true });
  update(); // 初期表示
})();

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

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

# 追加してほしい効果
Sticky スタックスクロール(レイアウト & グリッド)
position: sticky と top のずらしで、スクロールするとカードが重なり積み上がるストーリーテリング向けレイアウト。進捗バーをJSで連動。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- position: sticky スタック:スクロールでカードが重なり積み上がるレイアウト -->
<div class="ss">
  <p class="ss__hint">↓ スクロールしてカードを重ねる</p>
  <div class="ss__scroll" id="ssScroll">
    <section class="scard scard--1">
      <span class="scard__no">01</span>
      <h2>Sticky で重ねる</h2>
      <p>各カードに top を指定すると、上端で止まり次が覆い被さります。</p>
    </section>
    <section class="scard scard--2">
      <span class="scard__no">02</span>
      <h2>奥行きの演出</h2>
      <p>少しずつ縮小・回転させると立体的なスタックに見えます。</p>
    </section>
    <section class="scard scard--3">
      <span class="scard__no">03</span>
      <h2>JS不要が基本</h2>
      <p>進捗バーだけ JS で連動。レイアウト自体は CSS sticky のみ。</p>
    </section>
    <section class="scard scard--4">
      <span class="scard__no">04</span>
      <h2>最後のカード</h2>
      <p>ストーリーテリングや LP のヒーロー連結に最適です。</p>
    </section>
  </div>
  <div class="ss__progress" aria-hidden="true"><i id="ssBar"></i></div>
</div>

【CSS】
/* 全体:暗い背景にカラフルなスタックカード */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: "Hiragino Sans", system-ui, sans-serif;
  background: #0d1018; color: #f8fafc; height: 100vh; overflow: hidden;
}
.ss { position: relative; height: 100vh; display: flex; flex-direction: column; }
.ss__hint {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
  z-index: 5; font-size: 12px; color: #94a3b8; pointer-events: none;
  background: rgba(13,16,24,.7); padding: 4px 12px; border-radius: 20px;
}

/* スクロール領域 */
.ss__scroll {
  flex: 1; overflow-y: auto; scroll-behavior: smooth;
  padding: 48px 16px 60vh; /* 末尾に余白を作りスタックを成立させる */
  display: flex; flex-direction: column; gap: 26px; align-items: center;
}
.ss__scroll::-webkit-scrollbar { width: 8px; }
.ss__scroll::-webkit-scrollbar-thumb { background: #334155; border-radius: 8px; }

/* スタックカード:sticky で上端に貼り付く */
.scard {
  position: sticky; top: 24px;
  width: min(560px, 100%); min-height: 180px;
  border-radius: 18px; padding: 22px 24px;
  display: flex; flex-direction: column; gap: 8px; justify-content: center;
  box-shadow: 0 24px 50px -24px rgba(0,0,0,.7);
  color: #0b1020;
}
.scard__no { font-size: 12px; font-weight: 800; letter-spacing: .2em; opacity: .65; }
.scard h2 { font-size: clamp(20px, 4vw, 28px); font-weight: 800; }
.scard p { font-size: 14px; line-height: 1.7; max-width: 44ch; opacity: .9; }

/* 重なり時の段差を出すため top をずらす */
.scard--1 { top: 24px;  background: linear-gradient(135deg, #fda4af, #fb7185); }
.scard--2 { top: 38px;  background: linear-gradient(135deg, #a5b4fc, #818cf8); color: #f8fafc; }
.scard--3 { top: 52px;  background: linear-gradient(135deg, #6ee7b7, #34d399); }
.scard--4 { top: 66px;  background: linear-gradient(135deg, #fcd34d, #fbbf24); }

/* スクロール進捗バー */
.ss__progress {
  position: absolute; left: 0; right: 0; bottom: 0; height: 5px;
  background: #1e293b;
}
.ss__progress i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, #fb7185, #818cf8, #34d399);
  transition: width .1s linear;
}

@media (prefers-reduced-motion: reduce) {
  .ss__scroll { scroll-behavior: auto; }
  .ss__progress i { transition: none; }
}

【JavaScript】
// スクロール量に応じて進捗バーを更新(null安全・受動リスナで滑らかに)
(() => {
  const scroller = document.getElementById('ssScroll');
  const bar = document.getElementById('ssBar');
  if (!scroller || !bar) return;

  const update = () => {
    const max = scroller.scrollHeight - scroller.clientHeight;
    const ratio = max > 0 ? scroller.scrollTop / max : 0;
    bar.style.width = Math.min(100, ratio * 100).toFixed(1) + '%';
  };
  scroller.addEventListener('scroll', update, { passive: true });
  update(); // 初期表示
})();

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

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