JavaScriptのスクロールイベント(IntersectionObserver を使った動き)

「scrollY」と「IntersectionObserver」の違い

比較項目scrollYIntersectionObserver
処理方法毎回スクロールのたびに自分で位置を判定ブラウザが「見えたタイミング」で自動通知
パフォーマンス頻繁なスクロール処理は負担がかかる効率的で軽量、ブラウザ最適化されている
使用例「上へ戻るボタン」「ヘッダーの表示制御」など全体位置の制御「セクション表示」「ピン留め」など特定要素の監視
向いている場面全体的な動きの監視特定要素が「見えた/消えた」の判断

IntersectionObserverの主な構成とオプション

構文の基本

const observer = new IntersectionObserver(callback, options);
  • callback: 要素が表示された/消えたときに呼び出される関数
  • options: 監視の条件を設定するオブジェクト(省略可)

オプション(options)

オプション名意味
root要素 or null監視対象の交差判定を行う ビューポート要素null でブラウザ画面全体)
rootMargin文字列rootの上下左右に指定できる**余白(マージン)**例:"0px 0px -100px 0px"
threshold数値 or 配列交差と判定する割合(0〜1)。0.5なら「50%見えたら反応」
const options = {
  root: null, // ビューポート全体
  rootMargin: "0px 0px -10% 0px", // 下方向に少し早く発火
  threshold: 0.2 // 20%表示されたら反応
};

コールバックで使えるプロパティ(entry)

コールバック関数には entries という配列が渡され、それぞれの要素に対して以下のプロパティが使えます。

プロパティ名内容
entry.target要素監視している DOM 要素
entry.isIntersecting真偽値表示領域に入ったかどうか(trueなら見えている)
entry.intersectionRatio数値どのくらい見えているか(0〜1)
entry.boundingClientRectDOMRect要素の位置とサイズ
entry.intersectionRectDOMRect実際に可視になっている部分の矩形(交差している範囲)
entry.time数値(ms)観測開始からの経過時間(performance.now() と同様)
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.target);              // 対象要素
    console.log(entry.isIntersecting);      // 表示中か?
    console.log(entry.intersectionRatio);   // 表示されている割合
    console.log(entry.boundingClientRect);  // 位置とサイズ
  }
});

1.ヘッダー固定

固定されていないヘッダーはスクロールすると消えていきます。ある程度スクロールしたところで監視対象が見えてきたら、上部の上からふんわりと下がってきて固定されるヘッダーを作成してみましょう。

HTML/CSS/JavaScript

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>スクロールで固定ヘッダー出現</title>
    <style>
        /* 全体のリセット */
        body {
            margin: 0;
            font-family: sans-serif;
        }

        /* 通常表示されるヘッダー(上に流れていく) */
        .header-original {
            background: #57a2c7;
            color: white;
            padding: 20px;
            text-align: center;
            font-size: 24px;
        }

        /* 固定ヘッダー(最初は非表示) */
        .header-fixed {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            padding: 20px;
            background: #57a2c7d8;
            color: white;
            text-align: center;
            font-size: 24px;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
            transform: translateY(-100%);
            opacity: 0;
            pointer-events: none;
            transition: transform 0.6s ease, opacity 0.6s ease;
            z-index: 1000;
        }

        /* 表示状態になったとき(スライドイン+フェードイン) */
        .header-fixed.visible {
            transform: translateY(0);
            opacity: 1;
            pointer-events: auto;
        }

        /* marker:監視対象の透明な目印 */
        .marker {
            height: 1px;
            margin-top: 100px;
        }

        section {
            background: #eee;
            padding: 40px;
            border: 1px solid #ccc;
            max-width: 700px;
            margin: 20px auto 100px;
        }
    </style>
</head>

<body>

    <!-- 上に流れていく通常のヘッダー -->
    <header class="header-original">通常のヘッダー</header>

    <!-- 上部固定される別のヘッダー(最初は非表示) -->
    <header class="header-fixed">固定ヘッダー</header>

    <!-- この位置を監視することで切り替える -->
    <div class="marker"></div>

    <main>
        <section>セクション1</section>
        <section>セクション2</section>
        <section>セクション3</section>
        <section>セクション4</section>
        <section>セクション5</section>
        <div style="height: 600px;"></div>
    </main>

    <script>
        const fixedHeader = document.querySelector('.header-fixed');
        const marker = document.querySelector('.marker');

        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];

                if (!entry.isIntersecting) {
                    fixedHeader.classList.add('visible');
                } else {
                    fixedHeader.classList.remove('visible');
                }
            },
            {
                threshold: 0
            }
        );

        observer.observe(marker);

    </script>

</body>

</html>

実装の概要

このサンプルでは、通常のスクロールで流れていくヘッダーとは別に、画面上部に固定されるヘッダーをもう1つ用意しています。
特定の位置(.marker)をIntersectionObserverで監視し、その要素が見えなくなったときに固定ヘッダーを表示させる仕組みです。

HTMLの構成

<header class="header-original">通常のヘッダー</header>
<header class="header-fixed">固定ヘッダー</header>
<div class="marker"></div>
<main> ... </main>
  • .header-original は最初から見えていて、スクロールで上に流れていく。
  • .header-fixed は最初は非表示だが、ある位置を通過するとふわっと出現する。
  • .marker は「どこを境界にするか?」の目印。ここを監視します。

CSSのポイント

.header-fixed {
    position: fixed;
    transform: translateY(-100%);
    opacity: 0;
    transition: transform 0.6s ease, opacity 0.6s ease;
}
.header-fixed.visible {
    transform: translateY(0);
    opacity: 1;
}

  • .header-fixed画面外に隠れている状態translateY(-100%)opacity: 0)。
  • .visible クラスがつくと、上からスライドイン+フェードインします。
  • transition によりアニメーション効果が自然に行われます。

JavaScriptのポイント

const fixedHeader = document.querySelector('.header-fixed');
        const marker = document.querySelector('.marker');

        const observer = new IntersectionObserver(
            (entries) => {
                const entry = entries[0];

if (entry.isIntersecting) {
    fixedHeader.classList.remove('visible');
} else {
    fixedHeader.classList.add('visible');
}
            },
            {
                threshold: 0
            }
        );

        observer.observe(marker);
  • IntersectionObserver.marker画面に見えているか/いないかを自動で判定。
  • entry.isIntersectingは、IntersectionObserver が監視している要素(ここでは .marker)が 画面内に表示されているかどうかtrue または false で返すプロパティ。
  • .marker見えなくなったとき(=ある程度スクロールしたとき)、.visible を付けてヘッダーを表示。
  • 見える位置に戻ったら、ヘッダーは非表示に戻る。

ある要素の「表示/非表示」によってアニメーションしたいときは、IntersectionObserver が最適です。

2.スクロールしたら、上から下、右から左 左から右

スクロールしたタイミングで、監視対象が見えてきたら、上から下 右から左 左から右と登場するセクションを作成します。

HTML

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>セクションごとにフェードイン</title>
    <style>
        body {
            margin: 0;
            font-family: sans-serif;
            background: #f9f9f9;
        }

        section {
            max-width: 700px;
            margin: 100px auto 200px;
            padding: 100px 40px;
            border: 1px solid #ccc;
            background: #fff;
            opacity: 0;
            transform: translateY(300px);
            transition: all 1s ease;
        }

        /* 汎用:表示されたら共通でopacity 1に */
        .show {
            opacity: 1;
        }

        /* 方向別の初期位置 */
        .fadein-down {
            transform: translateY(300px);
        }

        .fadein-left {
            transform: translateX(-300px);
        }

        .fadein-right {
            transform: translateX(200px);
        }

        /* 表示されたらtransformをリセット */
        .fadein-down.show,
        .fadein-left.show,
        .fadein-right.show {
            transform: translate(0, 0);
        }

        h1 {
            margin: 0;
            background-color: #ccc;
            padding: 100px 0;
            text-align: center;
        }
    </style>
</head>

<body>
    <h1>タイトル</h1>
    <section class="fadein-down">下から上フェードイン</section>
    <section class="fadein-left">左から右にフェードイン</section>
    <section class="fadein-right">右から左にフェードイン</section>
    <section class="fadein-down">下から上フェードイン</section>
    <div style="height: 600px;"></div>

    <script>
        const targets = document.querySelectorAll('.fadein-down, .fadein-left, .fadein-right');

        const observer = new IntersectionObserver(
            (entries, obs) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        entry.target.classList.add('show');
                        // obs.unobserve(entry.target); // 一度だけ表示したら監視終了も可
                    } else {
                        entry.target.classList.remove('show'); // ← 上にスクロールした際に見えなくなったら元に戻す
                    }
                });
            },
            {
                threshold: 0.1
            }
        );

        targets.forEach(target => {
            observer.observe(target);
        });
    </script>


</body>

</html>

実装の概要

この例では、IntersectionObserver を使って、セクションが画面に表示されたタイミングでふわっと登場する動きを実装しています。

  • .fadein-down:下から上にスライドしながら表示
  • .fadein-left:左から右へスライド表示
  • .fadein-right:右から左へスライド表示

すべて同じ IntersectionObserver で監視しており、表示されると .show クラスを追加してアニメーションを開始します。

HTMLの構成

<section class="fadein-down">下から上フェードイン</section>
<section class="fadein-left">左から右にフェードイン</section>
<section class="fadein-right">右から左にフェードイン</section>
  • 各セクションには異なる方向のクラスが付けられており、CSSでそれぞれ異なる初期位置から表示されます。

CSSのポイント

section {
    opacity: 0;
    transform: translateY(300px);
    transition: all 1s ease;
}

  • すべてのセクションは最初は非表示&ずらされた位置にある
  • transition を使って滑らかに元の位置へ戻るように設定。

方向別のアニメーション

.fadein-down { transform: translateY(300px); }
.fadein-left { transform: translateX(-300px); }
.fadein-right { transform: translateX(200px); }

.fadein-down.show,
.fadein-left.show,
.fadein-right.show {
    transform: translate(0, 0);
    opacity: 1;
}

.show クラスが付くと opacity: 1transform: 元の位置に戻る

方向によって translateX/Y を使い分けて動きを変えています。

JavaScriptのポイント

    <script>
        const targets = document.querySelectorAll('.fadein-down, .fadein-left, .fadein-right');

        const observer = new IntersectionObserver(
            (entries, obs) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        entry.target.classList.add('show');
                        // obs.unobserve(entry.target); // 一度だけ表示したら監視終了も可
                    } else {
                        entry.target.classList.remove('show'); // ← 上にスクロールした際に見えなくなったら元に戻す
                    }
                });
            },
            {
                threshold: 0.1
            }
        );

        targets.forEach(target => {
            observer.observe(target);
        });
    </script>
  • .fadein-* クラスを持つすべてのセクションを監視。
  • isIntersectingtrue になると .show を付けて表示開始。
  • false の場合は .show を外して、再び非表示に戻す処理も追加されています。