Scroll Sync UI の作り方【JavaScriptで作る連動スクロールUI】

Scroll Sync UIのアイキャッチ画像

スクロールに合わせて、画像やテキストの内容が切り替わるUIを見たことはないでしょうか。

たとえば、左側にビジュアルを固定して、右側の文章をスクロールすると、表示内容が連動して変わるようなレイアウトです。
このような表現は、サービス紹介ページや機能説明ページ、ストーリー型のLPなどでよく使われます。

今回は、そんな Scroll Sync UI をシンプルに実装してみます。

この記事では、以下の流れで解説します。

  • Scroll Sync UI とは何か
  • どういう仕組みで作るのか
  • HTML / CSS / JavaScript の実装方法
  • 実験用デモで確認するポイント

「スクロール連動UIを作ってみたい」
「Sticky と IntersectionObserver を組み合わせた実装を理解したい」

という人に向けて、コピペしやすい形でまとめました。

「スクロール連動UIの基本から見たい場合は、先に Scroll Trigger AnimationSticky Scroll Animation を読むと入りやすいです。」

1.Scroll Sync UI とは?

Scroll Sync UI は、スクロール位置に応じて別の表示要素が同期して切り替わるUI のことです。

今回の例では、次のような構成にします。

  • 左側:表示エリア(画像やカード)
  • 右側:説明テキストのリスト
  • 右側をスクロールすると、左側の表示内容が連動して切り替わる

このUIの良いところは、単に情報を縦に並べるよりも、今どの内容を見ているのかが視覚的に分かりやすいことです。

特に以下のような場面で使いやすいです。

  • サービス機能の紹介
  • 手順やステップの説明
  • 特徴を順番に見せたいLP
  • ストーリー型のコンテンツ

2.今回の完成イメージ

今回作る Scroll Sync UI は、以下のような動きです。

  • 左側のパネルを sticky で固定
  • 右側に複数の説明ブロックを縦に並べる
  • 説明ブロックが画面中央付近に来たら、対応する左側パネルを active にする
  • active になったパネルだけを表示する

つまり、スクロール位置をきっかけに、表示中の内容を切り替える 仕組みです。

3.実装の考え方

今回のポイントは大きく3つです。

①左側は sticky で固定する

左側の表示エリアは、スクロールしてもその場に残るようにします。
これによって、右側の文章だけが流れ、左側の見た目が切り替わる形になります。

②右側にトリガーとなるセクションを並べる

右側には複数の説明ブロックを置きます。
それぞれに data-step を持たせて、左側の表示と対応づけます。

③IntersectionObserver で現在位置を検出する

各説明ブロックが画面内に入ったタイミングで、対応する左側パネルを active にします。
これで、スクロールに応じて表示を同期できます。

HTML

まずは全体の HTML です。

<section class="scroll-sync">
  <div class="sync-layout">
    <div class="sync-visual">
      <div class="visual-panel active" data-panel="1">
        <div class="panel-card card-1">
          <span class="panel-label">STEP 1</span>
          <h2>Discover</h2>
          <p>最初のセクションが表示されている状態です。</p>
        </div>
      </div>

      <div class="visual-panel" data-panel="2">
        <div class="panel-card card-2">
          <span class="panel-label">STEP 2</span>
          <h2>Focus</h2>
          <p>スクロールに合わせて表示内容が切り替わります。</p>
        </div>
      </div>

      <div class="visual-panel" data-panel="3">
        <div class="panel-card card-3">
          <span class="panel-label">STEP 3</span>
          <h2>Connect</h2>
          <p>テキストとビジュアルを連動させるUIです。</p>
        </div>
      </div>

      <div class="visual-panel" data-panel="4">
        <div class="panel-card card-4">
          <span class="panel-label">STEP 4</span>
          <h2>Finish</h2>
          <p>最後まで自然に読み進められる構成にできます。</p>
        </div>
      </div>
    </div>

    <div class="sync-content">
      <article class="sync-step" data-step="1">
        <p class="step-number">01</p>
        <h3>最初の導入を見せる</h3>
        <p>
          まずは最初の要素を表示しておきます。ユーザーがスクロールを始める前から、
          どんな内容なのかが視覚的に伝わる状態です。
        </p>
      </article>

      <article class="sync-step" data-step="2">
        <p class="step-number">02</p>
        <h3>視線を切り替える</h3>
        <p>
          次の説明ブロックが中央付近に入ると、左側の表示も切り替わります。
          文章を読む流れとビジュアルの変化を同期させるのがポイントです。
        </p>
      </article>

      <article class="sync-step" data-step="3">
        <p class="step-number">03</p>
        <h3>情報を整理して見せる</h3>
        <p>
          テキストだけを縦に並べるより、今見ている内容を大きな表示で支えられるので、
          説明系のUIと相性が良いです。
        </p>
      </article>

      <article class="sync-step" data-step="4">
        <p class="step-number">04</p>
        <h3>最後まで流れを維持する</h3>
        <p>
          1画面ずつ切り替えて見せる感覚を作れるので、LPやプロダクト紹介でも使いやすい表現です。
        </p>
      </article>
    </div>
  </div>
</section>

CSS

次に見た目を整えます。

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background: #0f1115;
  color: #f5f7fb;
}

.scroll-sync {
  padding: 80px 20px;
}

.sync-layout {
  max-width: 1100px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: start;
}

.sync-visual {
  position: sticky;
  top: 80px;
  height: 420px;
  border-radius: 24px;
  overflow: hidden;
  background: #171a21;
  border: 1px solid rgba(255, 255, 255, 0.08);
}

.visual-panel {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.45s ease, transform 0.45s ease;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.visual-panel.active {
  opacity: 1;
  transform: scale(1);
}

.panel-card {
  width: 100%;
  height: 100%;
  border-radius: 20px;
  padding: 32px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  color: #ffffff;
}

.card-1 {
  background: linear-gradient(135deg, #3b82f6, #1d4ed8);
}

.card-2 {
  background: linear-gradient(135deg, #10b981, #047857);
}

.card-3 {
  background: linear-gradient(135deg, #8b5cf6, #6d28d9);
}

.card-4 {
  background: linear-gradient(135deg, #f59e0b, #d97706);
}

.panel-label {
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0.12em;
  opacity: 0.8;
  margin-bottom: 12px;
}

.panel-card h2 {
  margin: 0 0 12px;
  font-size: 40px;
}

.panel-card p {
  margin: 0;
  max-width: 28em;
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.9);
}

.sync-content {
  display: flex;
  flex-direction: column;
  gap: 120px;
  padding: 40px 0;
}

.sync-step {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.step-number {
  margin: 0 0 12px;
  font-size: 14px;
  letter-spacing: 0.14em;
  color: #8ea3c7;
}

.sync-step h3 {
  margin: 0 0 16px;
  font-size: 32px;
  line-height: 1.3;
}

.sync-step p {
  margin: 0;
  line-height: 1.9;
  color: #c9d2e3;
}

@media (max-width: 900px) {
  .sync-layout {
    grid-template-columns: 1fr;
    gap: 24px;
  }

  .sync-visual {
    position: sticky;
    top: 20px;
    height: 280px;
  }

  .sync-content {
    gap: 72px;
    padding: 0;
  }

  .sync-step {
    min-height: auto;
  }

  .panel-card h2 {
    font-size: 28px;
  }

  .sync-step h3 {
    font-size: 24px;
  }
}

JavaScript

最後に、スクロール位置に応じて active を切り替えます。

const steps = document.querySelectorAll('.sync-step');
const panels = document.querySelectorAll('.visual-panel');

const setActivePanel = (step) => {
  panels.forEach((panel) => {
    panel.classList.toggle(
      'active',
      panel.dataset.panel === step
    );
  });
};

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const step = entry.target.dataset.step;
        setActivePanel(step);
      }
    });
  },
  {
    threshold: 0.5
  }
);

steps.forEach((step) => observer.observe(step));

4.仕組みを整理するとどうなっている?

今回の実装は、かなりシンプルです。

まず、右側の各説明ブロックに data-step="1" のような番号を付けています。
左側のパネルにも data-panel="1" のように対応する番号を付けています。

そして、IntersectionObserver を使って、

  • どの説明ブロックが見えているか
  • そのブロックの番号は何か

を取得し、対応する左側パネルだけに active を付けています。

つまり本質的には、

「スクロール位置を見て、表示中の番号を切り替えているだけ」

です。

難しそうに見えるUIでも、こうして分解するとかなり理解しやすくなります。

5.この実装のポイント

sticky と absolute の組み合わせが分かりやすい

左側の表示エリアは sticky で固定しています。
その中の各パネルは absolute で重ねて配置しています。

これによって、領域自体は固定されたまま、中身だけをフェードで切り替える ことができます。

active の切り替えだけに絞ると扱いやすい

JavaScript 側では、複雑な座標計算はしていません。
今見えている要素に対応したパネルへ active を付けるだけです。

この考え方にしておくと、あとから以下のような拡張もしやすいです。

  • 背景色も連動して変える
  • 進捗インジケーターを付ける
  • テキストや画像をもっと増やす
  • アニメーションを少し強くする

threshold の調整で体感が変わる

今回の observer では threshold: 0.5 にしています。
これは、対象要素の 50% くらいが見えたら切り替える、という意味です。

ここを変えると、切り替えのタイミングも変わります。

  • 早めに切り替えたい → 0.3
  • しっかり中央に来てから切り替えたい → 0.6 や 0.7

6.よくある失敗

①sticky が効かない

親要素に overflow: hidden; や overflow: auto; が付いていると、sticky が思った通りに動かないことがあります。
Sticky が効かないときは、まず親要素の overflow を確認すると原因を見つけやすいです。

②スマホで高さが足りない

デスクトップでは見やすくても、スマホでは sticky エリアが大きすぎて圧迫感が出ることがあります。
そのため、メディアクエリで高さを小さくしたり、余白を減らしたりするのが大切です。

③切り替えが頻繁すぎる

セクションの高さが短いと、スクロール時に表示が落ち着かなくなることがあります。
今回のように、各説明ブロックにある程度の高さを持たせると、切り替えが安定しやすくなります。

7.Scroll Story UI との違い

以前の Scroll Story UI では、スクロールに応じて文章や状態が順番に切り替わる構成が中心でした。
今回の Scroll Sync UI は、それをもう少し実務寄りにして、説明テキストとビジュアルを並列で同期させる形に寄せています。

ざっくり言うと、

  • Scroll Story UI
    → ストーリーや流れを順番に見せる
  • Scroll Sync UI
    → 説明と表示を連動させて理解しやすくする

という違いがあります。

この2つはかなり近い考え方なので、合わせて読むと理解しやすいです。

8.実務で使うならどう応用する?

このパターンは、実務でも使いやすいです。

たとえば以下のようなページに向いています。

  • SaaS の機能紹介
  • アプリの画面説明
  • 制作実績の見せ方
  • 商品の特徴説明
  • 手順のステップ紹介

特に、「文章だけでは弱いけれど、画像を並べるだけでも伝わりにくい」という場面で便利です。
説明文とビジュアルを1セットとして見せたいときに強いUI だと言えます。

9.実験:Scroll Sync UI を実際に触ってみる

パターン1

以下は、今回紹介した Scroll Sync UI を実際に試せるデモです。
右側のテキストをスクロールすると、左側(上側)の表示パネルが連動して切り替わります。
まずは動きを見ながら、「どの位置で切り替わるか」 を観察してみてください。

パターン2

文章とビジュアルを同期して見せると、単なる縦並びよりも内容を追いやすくなります。
このデモでは、右側の説明ブロックに合わせて左側(上側)の表示が切り替わる構成を試せます。

パターン3

Scroll Sync UI は、サービス紹介やステップ説明と相性の良いパターンです。
デモを触りながら、sticky で固定した表示エリアスクロール中の説明ブロックがどう連動しているかを確認してみてください。

観察ポイント

  • 情報の流れが自然に感じられるか
  • 左側(上側)が固定されることで、今見ている内容が理解しやすくなるか
  • 単純な縦並びよりも、視線誘導がしやすいか

10.まとめ

今回は、Scroll Sync UI の基本的な作り方を紹介しました。

ポイントは次の3つです。

  • 左側を sticky で固定する
  • 右側にスクロール用の説明ブロックを並べる
  • IntersectionObserver で表示を同期する

この形を覚えておくと、単なるスクロール演出としてではなく、情報を分かりやすく見せるためのUI として応用しやすくなります。

見た目は少し凝って見えますが、やっていることはそこまで複雑ではありません。
まずは今回のシンプルな構成を動かしてみて、そこから色や画像、アニメーションを少しずつ足していくのがおすすめです。


Scroll Animationシリーズ

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA