stickyスクロールストーリー

position:stickyでビジュアルを固定したまま、スクロールに連動して中身を切り替えるストーリーテリングUI。

#css#javascript#sticky

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk 機能紹介。左のビジュアルを固定し、右の説明をスクロールで切替 -->
<div class="fd-scroller" id="storyScroller">
  <div class="fd-layout">
    <!-- sticky で固定されるビジュアル -->
    <aside class="fd-sticky">
      <div class="fd-window">
        <div class="fd-window-bar">
          <span></span><span></span><span></span>
          <em class="fd-window-title">FlowDesk</em>
        </div>
        <div class="fd-art" id="storyArt">
          <span class="fd-emoji" id="storyEmoji">&#128202;</span>
          <span class="fd-art-label" id="storyLabel">STEP 1</span>
        </div>
      </div>
    </aside>

    <!-- スクロールで進む説明ステップ -->
    <div class="fd-steps">
      <section class="fd-step is-active" data-step="0" data-color="#0f1b34" data-emoji="&#128202;">
        <span class="fd-step-no">01</span>
        <h2>すべての業務を1つに</h2>
        <p>タスク、案件、メンバーの状況をひとつのダッシュボードへ。点在していた情報が、今日やることへと整理されます。</p>
      </section>
      <section class="fd-step" data-step="1" data-color="#16284a" data-emoji="&#9889;">
        <span class="fd-step-no">02</span>
        <h2>自動化でムダを削減</h2>
        <p>「申請が来たら担当へ通知」など、繰り返しの作業はルールで自動化。チームは本質的な仕事に集中できます。</p>
      </section>
      <section class="fd-step" data-step="2" data-color="#1d3a6b" data-emoji="&#128101;">
        <span class="fd-step-no">03</span>
        <h2>チームの今が見える</h2>
        <p>誰が何を抱えているかを可視化。負荷の偏りに気づき、先回りで助け合える体制に。</p>
      </section>
      <section class="fd-step" data-step="3" data-color="#2451a6" data-emoji="&#128640;">
        <span class="fd-step-no">04</span>
        <h2>14日間、無料で試す</h2>
        <p>クレジットカードは不要。まずは小さなチームから、FlowDesk のある毎日を体験してください。</p>
      </section>
    </div>
  </div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
  background: #0a1226;
  color: var(--white);
  -webkit-font-smoothing: antialiased;
}

/* 内部スクロール領域 */
.fd-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--blue) transparent;
  background: #0a1226;
}

.fd-layout {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  max-width: 640px;
  margin: 0 auto;
  padding: 0 18px;
}

/* 固定されるビジュアル */
.fd-sticky {
  position: sticky;
  top: 0;
  flex: 0 0 46%;
  height: 100vh;
  max-height: 100%;
  display: flex;
  align-items: center;
}
.fd-window {
  width: 100%;
  border-radius: 14px;
  overflow: hidden;
  background: #0e1a33;
  border: 1px solid rgba(79,124,255,.25);
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.9);
}
.fd-window-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 12px;
  background: #0b1430;
  border-bottom: 1px solid rgba(255,255,255,.06);
}
.fd-window-bar span { width: 8px; height: 8px; border-radius: 50%; background: #38456b; }
.fd-window-title { margin-left: 8px; font-size: .66rem; letter-spacing: .12em; color: #8ea3d6; font-style: normal; }

.fd-art {
  position: relative;
  height: 180px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--navy);
  transition: background .5s ease;
}
.fd-emoji {
  font-size: 3rem;
  line-height: 1;
  transition: transform .4s cubic-bezier(.2,.8,.2,1);
}
.fd-art-label {
  font-size: .62rem;
  letter-spacing: .26em;
  color: var(--blue);
  font-weight: 700;
}

/* スクロールする説明側 */
.fd-steps {
  flex: 1 1 auto;
  padding: 60px 0;
}
.fd-step {
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 18px 6px;
  opacity: .35;
  transform: translateY(10px);
  transition: opacity .45s ease, transform .45s ease;
}
.fd-step.is-active { opacity: 1; transform: none; }
.fd-step-no {
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .1em;
  color: var(--blue);
}
.fd-step h2 { font-size: 1.3rem; font-weight: 800; margin: 8px 0 10px; }
.fd-step p { font-size: .9rem; line-height: 1.85; color: #b9c4e2; }

@media (prefers-reduced-motion: reduce) {
  .fd-step { opacity: 1; transform: none; }
  .fd-emoji, .fd-art { transition: none; }
}
JavaScript
// FlowDesk 機能紹介:中央に来たステップに合わせて固定ビジュアルを更新
(() => {
  const scroller = document.getElementById('storyScroller');
  const steps = Array.from(document.querySelectorAll('.fd-step'));
  const art = document.getElementById('storyArt');
  const emoji = document.getElementById('storyEmoji');
  const label = document.getElementById('storyLabel');
  if (!scroller || !steps.length) return; // null安全

  function activate(step) {
    steps.forEach(s => s.classList.toggle('is-active', s === step));
    if (art) art.style.background = step.dataset.color || '#0f1b34';
    if (emoji) {
      emoji.innerHTML = step.dataset.emoji || '';
      // 切替時に小さくバウンス
      emoji.style.transform = 'scale(0.6)';
      requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
    }
    if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
  }

  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) activate(entry.target);
      });
    }, { root: scroller, threshold: 0.6 });
    steps.forEach(s => io.observe(s));
  }
  activate(steps[0]); // 初期状態

  // 操作がなくても切替が見えるよう、ゆっくり自動スクロール
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 2;
      requestAnimationFrame(step);
    }, 700);
  }
})();

コード

HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="story-scroller" id="storyScroller">
  <div class="sticky-story">
    <!-- 左側:sticky で固定されるビジュアル -->
    <div class="story-visual">
      <div class="story-art" id="storyArt">
        <span class="story-step-label" id="storyLabel">STEP 1</span>
        <span class="story-emoji" id="storyEmoji">&#127793;</span>
      </div>
    </div>

    <!-- 右側:スクロールするステップ群 -->
    <div class="story-steps">
      <section class="story-step" data-step="0" data-emoji="&#127793;" data-color="#2e7d4f">
        <h2>種をまく</h2>
        <p>小さな一粒から物語は始まります。土の中で静かに準備を整える時間。</p>
      </section>
      <section class="story-step" data-step="1" data-emoji="&#127807;" data-color="#3f8c5a">
        <h2>芽が出る</h2>
        <p>左のビジュアルがスクロールに連動して切り替わります。stickyで固定したまま中身だけ変化。</p>
      </section>
      <section class="story-step" data-step="2" data-emoji="&#127804;" data-color="#c9962b">
        <h2>花が咲く</h2>
        <p>各ステップが枠の中央に来た瞬間、IntersectionObserverが状態を更新します。</p>
      </section>
      <section class="story-step" data-step="3" data-emoji="&#127795;" data-color="#2e6b8c">
        <h2>大きく育つ</h2>
        <p>製品紹介・チュートリアル・ストーリーテリングに最適な定番レイアウト。</p>
      </section>
    </div>
  </div>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #f4f1ec;
  color: #23231f;
}

/* プレビュー枠を埋める自前スクロール領域 */
.story-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.sticky-story {
  display: grid;
  grid-template-columns: 1fr 1fr;
  max-width: 920px;
  margin: 0 auto;
  gap: 20px;
  padding: 0 20px;
}

/* 左:固定ビジュアル(スクロール領域に貼り付く) */
.story-visual {
  position: relative;
}
.story-art {
  position: sticky;
  top: 0;
  height: 100vh;
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #2e7d4f; /* JSで上書き */
  transition: background .6s ease;
}
.story-emoji {
  font-size: 5rem;
  filter: drop-shadow(0 8px 18px rgba(0,0,0,.25));
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.story-step-label {
  position: absolute;
  top: 14px; left: 14px;
  padding: 5px 12px;
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .12em;
  color: #fff;
  background: rgba(0,0,0,.28);
  border-radius: 999px;
  z-index: 3;
  pointer-events: none;
}

/* 右:スクロールするステップ */
.story-steps { padding: 40vh 0; }
.story-step {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: .35;
  transition: opacity .5s ease;
}
.story-step.is-active { opacity: 1; }
.story-step h2 {
  font-size: 1.5rem;
  font-weight: 800;
  margin-bottom: 10px;
}
.story-step p { line-height: 1.7; color: #4b4b44; font-size: .92rem; }

@media (prefers-reduced-motion: reduce) {
  .story-art, .story-emoji, .story-step { transition: none; }
}
JavaScript
// 自前スクロール領域内で、中央に来たステップに合わせてビジュアルを更新
(() => {
  const scroller = document.getElementById('storyScroller');
  const steps = Array.from(document.querySelectorAll('.story-step'));
  const art = document.getElementById('storyArt');
  const emoji = document.getElementById('storyEmoji');
  const label = document.getElementById('storyLabel');
  if (!scroller || !steps.length) return; // null安全

  function activate(step) {
    steps.forEach(s => s.classList.toggle('is-active', s === step));
    if (art) art.style.background = step.dataset.color || '#2e7d4f';
    if (emoji) {
      emoji.innerHTML = step.dataset.emoji || '';
      // 切替時に小さくバウンス
      emoji.style.transform = 'scale(0.6)';
      requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
    }
    if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
  }

  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) activate(entry.target);
      });
    }, { root: scroller, threshold: 0.6 });
    steps.forEach(s => io.observe(s));
  }
  activate(steps[0]); // 初期状態

  // 操作がなくても切替が見えるよう、ゆっくり自動スクロール
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 2;
      requestAnimationFrame(step);
    }, 700);
  }
})();

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

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

# 追加してほしい効果
stickyスクロールストーリー(スクロール演出)
position:stickyでビジュアルを固定したまま、スクロールに連動して中身を切り替えるストーリーテリングUI。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="story-scroller" id="storyScroller">
  <div class="sticky-story">
    <!-- 左側:sticky で固定されるビジュアル -->
    <div class="story-visual">
      <div class="story-art" id="storyArt">
        <span class="story-step-label" id="storyLabel">STEP 1</span>
        <span class="story-emoji" id="storyEmoji">&#127793;</span>
      </div>
    </div>

    <!-- 右側:スクロールするステップ群 -->
    <div class="story-steps">
      <section class="story-step" data-step="0" data-emoji="&#127793;" data-color="#2e7d4f">
        <h2>種をまく</h2>
        <p>小さな一粒から物語は始まります。土の中で静かに準備を整える時間。</p>
      </section>
      <section class="story-step" data-step="1" data-emoji="&#127807;" data-color="#3f8c5a">
        <h2>芽が出る</h2>
        <p>左のビジュアルがスクロールに連動して切り替わります。stickyで固定したまま中身だけ変化。</p>
      </section>
      <section class="story-step" data-step="2" data-emoji="&#127804;" data-color="#c9962b">
        <h2>花が咲く</h2>
        <p>各ステップが枠の中央に来た瞬間、IntersectionObserverが状態を更新します。</p>
      </section>
      <section class="story-step" data-step="3" data-emoji="&#127795;" data-color="#2e6b8c">
        <h2>大きく育つ</h2>
        <p>製品紹介・チュートリアル・ストーリーテリングに最適な定番レイアウト。</p>
      </section>
    </div>
  </div>
</div>

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

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #f4f1ec;
  color: #23231f;
}

/* プレビュー枠を埋める自前スクロール領域 */
.story-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.sticky-story {
  display: grid;
  grid-template-columns: 1fr 1fr;
  max-width: 920px;
  margin: 0 auto;
  gap: 20px;
  padding: 0 20px;
}

/* 左:固定ビジュアル(スクロール領域に貼り付く) */
.story-visual {
  position: relative;
}
.story-art {
  position: sticky;
  top: 0;
  height: 100vh;
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #2e7d4f; /* JSで上書き */
  transition: background .6s ease;
}
.story-emoji {
  font-size: 5rem;
  filter: drop-shadow(0 8px 18px rgba(0,0,0,.25));
  transition: transform .5s cubic-bezier(.2,.8,.2,1);
}
.story-step-label {
  position: absolute;
  top: 14px; left: 14px;
  padding: 5px 12px;
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .12em;
  color: #fff;
  background: rgba(0,0,0,.28);
  border-radius: 999px;
  z-index: 3;
  pointer-events: none;
}

/* 右:スクロールするステップ */
.story-steps { padding: 40vh 0; }
.story-step {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: .35;
  transition: opacity .5s ease;
}
.story-step.is-active { opacity: 1; }
.story-step h2 {
  font-size: 1.5rem;
  font-weight: 800;
  margin-bottom: 10px;
}
.story-step p { line-height: 1.7; color: #4b4b44; font-size: .92rem; }

@media (prefers-reduced-motion: reduce) {
  .story-art, .story-emoji, .story-step { transition: none; }
}

【JavaScript】
// 自前スクロール領域内で、中央に来たステップに合わせてビジュアルを更新
(() => {
  const scroller = document.getElementById('storyScroller');
  const steps = Array.from(document.querySelectorAll('.story-step'));
  const art = document.getElementById('storyArt');
  const emoji = document.getElementById('storyEmoji');
  const label = document.getElementById('storyLabel');
  if (!scroller || !steps.length) return; // null安全

  function activate(step) {
    steps.forEach(s => s.classList.toggle('is-active', s === step));
    if (art) art.style.background = step.dataset.color || '#2e7d4f';
    if (emoji) {
      emoji.innerHTML = step.dataset.emoji || '';
      // 切替時に小さくバウンス
      emoji.style.transform = 'scale(0.6)';
      requestAnimationFrame(() => { emoji.style.transform = 'scale(1)'; });
    }
    if (label) label.textContent = 'STEP ' + (Number(step.dataset.step) + 1);
  }

  if ('IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) activate(entry.target);
      });
    }, { root: scroller, threshold: 0.6 });
    steps.forEach(s => io.observe(s));
  }
  activate(steps[0]); // 初期状態

  // 操作がなくても切替が見えるよう、ゆっくり自動スクロール
  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  let auto = !reduce;
  const stop = () => { auto = false; };
  ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
    scroller.addEventListener(ev, stop, { passive: true }));

  if (auto) {
    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 2;
      requestAnimationFrame(step);
    }, 700);
  }
})();

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

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