フロントエンド 2026.07.22

Intersection Observer APIだけでできる!スクロール連動アニメーション実践パターン集

約13分で読めます

jQueryやScrollMagicに頼らず、Intersection Observer APIだけでスクロール連動アニメーションを実装する実践パターンを紹介。コード例と失敗談も交えて解説します。

こんな悩みはありませんか?

「スクロールしたら要素がふわっと現れるアニメーションを実装したいけど、jQueryのプラグインは重いし、ScrollMagicは設定が複雑で…」

「Intersection Observer APIって聞いたことあるけど、実務でどこまで使えるのか正直わからない」

フロントエンドの現場では、こうした声をよく耳にします。実際、スクロール連動アニメーションはUXを向上させる強力な演出である一方、ライブラリ選定やパフォーマンスの問題で実装が迷走しがちです。

この記事では、Fivenineが実案件で使ってきた Intersection Observer APIのみを使ったスクロールアニメーション実装パターンを具体的なコードと失敗談を交えてご紹介します。外部ライブラリを使わず、ネイティブAPIだけでここまでできるという実感を持っていただけるはずです。


なぜ今、Intersection Observer APIなのか

以前のスクロールイベント(window.addEventListener('scroll', ...))は、スクロールのたびに大量の計算が走り、メインスレッドを圧迫してカクつきの原因になりやすい実装でした。getBoundingClientRect()を毎フレーム呼ぶと強制レイアウトが発生し、FPS低下を招くこともしばしばです。

Intersection Observer APIはこの問題を根本から解決します。要素がビューポートに入ったかどうかをブラウザが非同期で監視してくれるため、開発者側でスクロール量を計算する必要がありません。

主なメリットをまとめると以下の通りです。

  • パフォーマンスに優れる: メインスレッドを妨げない非同期処理
  • コードがシンプル: 監視・コールバック・解除の3ステップで完結
  • ブラウザ対応が広い: Edge、Chrome、Firefox、Safari全対応(IE除く)
  • 外部依存ゼロ: ライブラリ不要でバンドルサイズへの影響なし

あるランディングページ案件では、ScrollMagicからIntersection Observer APIへ移行したことで、Lighthouseのパフォーマンススコアが68点から89点に改善した事例があります。アニメーションの演出を落とすことなく、ライブラリを削除するだけでこれだけの差が出るのは、現場で体感して初めてわかる驚きでした。


フロントエンド開発をお探しですか?

React・Vue・モダンなUI/UX開発をサポートします

無料で相談する

実践パターン集

パターン1: フェードインアップ(最も汎用的)

スクロールすると要素が下から浮き上がりながらフェードインする、最もよく使われるパターンです。

<!-- HTML -->
<div class="fade-up">コンテンツ</div>
<div class="fade-up">コンテンツ</div>
<div class="fade-up">コンテンツ</div>
/* CSS */
.fade-up {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.fade-up.is-visible {
  opacity: 1;
  transform: translateY(0);
}
// JavaScript
const targets = document.querySelectorAll('.fade-up');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      // 一度表示したら監視を解除してパフォーマンスを維持
      observer.unobserve(entry.target);
    }
  });
}, {
  threshold: 0.1,       // 10%見えたらトリガー
  rootMargin: '0px 0px -50px 0px' // 下端から50px手前で発火
});

targets.forEach((el) => observer.observe(el));

ポイントは observer.unobserve() です。表示後も監視し続けると無駄なコールバックが走り続けるため、一度表示したら即解除するのが鉄則です。


パターン2: 遅延付きスタガーアニメーション

カード要素を順番にずらして表示する「スタガー(千鳥)」アニメーションです。一覧ページやセクションの視覚的な印象が大きく変わります。

// data-delay属性でCSSカスタムプロパティを設定
const cards = document.querySelectorAll('.card');

cards.forEach((card, index) => {
  card.style.setProperty('--delay', `${index * 0.1}s`);
});

const staggerObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      staggerObserver.unobserve(entry.target);
    }
  });
}, { threshold: 0.05 });

cards.forEach((card) => staggerObserver.observe(card));
.card {
  opacity: 0;
  transform: translateY(30px);
  transition:
    opacity 0.5s ease var(--delay, 0s),
    transform 0.5s ease var(--delay, 0s);
}

.card.is-visible {
  opacity: 1;
  transform: translateY(0);
}

パターン3: カウントアップアニメーション

数字が視野に入ったタイミングで0からカウントアップする演出です。実績数や統計を印象付けたいセクションで特に効果的です。

<span class="countup" data-target="1500">0</span>
const countupObserver = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    const el = entry.target;
    const target = parseInt(el.dataset.target, 10);
    const duration = 1500; // ms
    const startTime = performance.now();

    const tick = (now) => {
      const elapsed = now - startTime;
      const progress = Math.min(elapsed / duration, 1);
      // イージング(ease-out)
      const eased = 1 - Math.pow(1 - progress, 3);
      el.textContent = Math.floor(eased * target).toLocaleString();

      if (progress < 1) requestAnimationFrame(tick);
    };

    requestAnimationFrame(tick);
    countupObserver.unobserve(el);
  });
}, { threshold: 0.5 });

document.querySelectorAll('.countup')
  .forEach((el) => countupObserver.observe(el));

イージング計算に requestAnimationFrame を組み合わせることで、なめらかな数字の変化を実現しています。


よくある失敗パターンと対処法

実案件で何度も遭遇した失敗をまとめました。「なぜか動かない」という場面の大半はこれで解決します。

ページロード時にすでにビューポート内に入っている要素は、スクロールしなくても `isIntersecting: true` でコールバックが呼ばれます。これは仕様通りの動作ですが、「ページを開いたら最初の要素がアニメーションなしで見えてほしい」という場合は、`rootMargin` を調整するか、ページロード直後の要素に `is-visible` を付与しておく設計にしましょう。
スマホ画面は縦に長く、横幅が狭いため、`threshold: 0.5` のような高い値を設定すると要素が半分以上見えないとトリガーされません。縦に長いカード要素では `threshold: 0.1` 〜 `0.2` 程度にとどめるのが安全です。
監視解除を忘れると、スクロールのたびにコールバックが走り続けます。「一度だけ実行したい」ケースでは必ず `unobserve()` を呼ぶことを習慣化してください。eslintのカスタムルールで縛るチームもあります。
inline styleでopacityをJSから操作しつつ、CSSにも `transition` を書くと競合します。「状態の付与はJS(クラスの付け外し)、スタイルの定義はCSS」と役割を明確に分けることで、メンテナンス性も上がります。
ユーザーの「アニメーションを減らす」設定(`prefers-reduced-motion`)を無視したアニメーションはアクセシビリティ違反になりえます。必ずメディアクエリで配慮しましょう。
```css
@media (prefers-reduced-motion: reduce) {
.fade-up {
opacity: 1;
transform: none;
transition: none;
}
}
```

各パターンの活用シーン比較

パターン実装難易度モバイル対応推奨シーン
フェードインアップほぼ全ページ
スタガーアニメーションカード・一覧ページ
カウントアップ実績・数値訴求
パララックス(別途実装)LPのファーストビュー

パララックスは別途 scroll イベントとの組み合わせが必要で、モバイルでのパフォーマンス劣化リスクが高いため、Intersection Observerの守備範囲外として扱うのが現実的です。


フロントエンド開発をお探しですか?

React・Vue・モダンなUI/UX開発をサポートします

無料で相談する

Web制作・UI改善もお任せください

フロントエンド開発

モダンな技術で、使いやすく美しいWebサイトを実現します

200件以上の制作実績 顧客満足度97% 初回相談無料

※ 通常1営業日以内にご返信します

まとめと次のステップ

Intersection Observer APIは、「スクロールイベントの複雑な計算をブラウザに委ねる」という設計思想が秀逸です。コード量が少なく、パフォーマンスが高く、保守しやすい。実務での採用価値は十分あります。

最初の一歩は「フェードインアップ」の1パターンだけを既存プロジェクトに導入してみることです。HTMLに .fade-up クラスを追加して、CSSとJSを数十行書くだけで、サイトの印象は大きく変わります。

ただし、アニメーションの設計はサイト全体のブランドトーンやUXフローと切り離せません。「どこで・何を・どのタイミングで動かすか」の設計を誤ると、かえって煩わしい体験になることも。実装と同時に、演出設計の視点も大切にしてください。

「既存サイトのアニメーション設計を見直したい」「パフォーマンスを落とさずに表現を豊かにしたい」といったご要望があれば、お気軽にFivenine Designまでご相談ください。

この記事をシェア

Webサイトの改善、お任せください

デザイン改善・表示速度向上・レスポンシブ対応など、成果の出るWeb制作を行います。 初回相談は無料です。

※ 1営業日以内にご返信いたします

この技術でお困りなら

無料でプロに相談できます

相談する
AIに無料相談