Knowledge Diary

Intersection Observer API(交差オブザーバーAPI)とスクロールイベントでインビューアニメーションを実装する方法!【JavaScript】

Published: Updated:

Intersection Observer API(交差オブザーバーAPI)とスクロールイベントでインビューアニメーションを実装する方法!【JavaScript】

こんにちは、Webデザイナーの 夢拓(MUHIRO)です。

スクロールをして表示領域に要素が入ったらアニメーションを開始させたい(この記事ではインビューアニメーションと呼びます)場面はありませんか?

インビューアニメーションをする場合、スクロールイベントを使用することが多いかと思います。
しかし、スクロールイベントはスクロールのたびに発火し、パフォーマンスに影響を与える可能性があります。

そこで、今回の記事では
交差オブザーバーAPI(Intersection Observer API)を活用し、パフォーマンスを向上させたインビューアニメーションについて解説をします!

ただ、交差オブザーバーAPI単体では求めた動作にならない場合もあるため、スクロールイベントと組み合わせて紹介をします!

この記事はこんな方におすすめ!

  • スクロールしたら要素を表示させる方法を知りたい方
  • 交差オブザーバーAPI(Intersection Observer API)について知りたい方

この記事を読めば、スクロールイベントに代わる交差オブザーバーAPIの活用法や、両者を組み合わせて実装する具体的な手法がわかり、パフォーマンスの向上につながるインビューアニメーションを実現できるようになります。
ぜひ最後までご覧ください!

それでは、どうぞ!

Index

CloseOpen

Intersection Observer API(交差オブザーバーAPI)とは?

Intersection Observer API(交差オブザーバーAPI)は、要素がビューポート(表示領域)にどの程度表示されているかを効率的に監視するためのAPIです。
このAPIを活用することで、要素が画面に表示されるときや非表示になったときにアニメーションやアクションを実行することができます。

詳しくは、以下の記事で解説をしてます!

Intersection Observer APIとスクロールイベントを組み合わせた理由

Intersection Observer APIとスクロールイベントを組み合わせた記述を解説します。

Intersection Observer APIについての理解がないとわかりづらい部分も多いため「 【JavaScript】Intersection Observer API(交差オブザーバーAPI)とは?使い方とポイントの解説」の記事もぜひ読んでください!

なぜ、スクロールイベントと組み合わせるのか?

Intersection Observer API単体で制御しようと思いましたが、想定した動作しなかったためです。
具体的には、複数の要素で処理を行った場合、アニメーション開始のタイミングがバラバラになってしまうことです。

Intersection Observer API単体でできない理由

Intersection Observer APIのコールバックが呼び出されるタイミングが監視した要素に依存するからです。

例えば、監視した要素が画面高さの50%に表示されたタイミングでアニメーションを開始させたい場合、監視する要素の高さがビューポートの高さ以上でなければ、想定より早くアニメーションが開始されることがあります。

ビューポートと交差したタイミングでスクロールイベントを発火させる

私のたどり着いた答えは、ビューポートと交差したタイミングでスクロールイベントを発火させて、画面高さ50%のスクロールしたらインビューアニメーションをさせる方法です。

Intersection Observer APIとスクロールイベントを組み合わせた記述方法

インビューアニメーションをするまでの処理手順

  1. “IntersectionObserver”を使用して要素を監視、画面内に表示タイミングで処理
  2. 画面内に表示したら、スクロールイベントを開始
  3. 表示タイミングまでスクロールしたらインビューアニメーションの処理を開始

画面内に入ってから、スクロールイベントを開始することとで、画面高さでインビューアニメーションの開始タイミングを制御します。

デモ

デフォルト状態で背景色は青色。画面高さ50%に要素が入ってきたら背景色が赤色に変化します。
また、読み込み時に画面50%に要素が入っている場合も、背景色が赤色に変化します。

See the Pen Untitled by Muhiro (@muhiro) on CodePen.

ソースコード

const inviewEvent = new Map();
const inviewedScrollEvent = (e) => {
    const targetRect = e.getBoundingClientRect();

    const addClass = () =>{
        e.classList.add('is-inviewed');
        const scrollListener = inviewEvent.get(e);
        window.removeEventListener('scroll', scrollListener);
        inviewEvent.delete(e);
    }

    if(targetRect.height < wh * .5) addClass();

    const scrollPosition = window.scrollY || document.documentElement.scrollTop;
    const elementTop = targetRect.top + scrollPosition - wh;
    const elementEnd = elementTop + targetRect.height;
    const scrollAmount = scrollPosition - elementTop;
    if(scrollAmount <= 0 && !e.classList.contains('is-inviewed')) {
        const scrollListener = inviewEvent.get(e);
        window.removeEventListener('scroll', scrollListener);
        inviewEvent.delete(e);
    }
    if(wh * .5 <= scrollAmount && elementEnd >= scrollAmount) {
        addClass();
    }
}

const inviewElm = document.querySelectorAll('[data-inview]');

const inVIew = (entries,observer) => {
    entries.forEach( (entry) => {
        if (entry.isIntersecting) {
            inviewedScrollEvent(entry.target);
            if(!inviewEvent.has(entry.target) && !entry.target.classList.contains('is-inviewed')) {
                const scrollEvent = () => inviewedScrollEvent(entry.target);
                window.addEventListener('scroll', scrollEvent);
                inviewEvent.set(entry.target,scrollEvent);
            }
        }
    });
};
const inViewObserver = new IntersectionObserver(inVIew, {
    root: null,
    rootMargin: '0px' ,
    threshold: 0
});
inviewElm.forEach(e => {
    inViewObserver.observe(e);
});

ソースコードの詳しい解説

    const inviewEvent = new Map();

特定の要素とそのスクロールイベントリスナーを関連付けて管理するために、Mapオブジェクトを使用します。
これにより、画面内に入った要素ごとにイベントリスナーを動的に追加・削除することが容易になります。


const inVIew = (entries,observer) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            inviewedScrollEvent(entry.target);
            if(!inviewEvent.has(entry.target) && !entry.target.classList.contains('is-inviewed')) {
                const scrollEvent = () => inviewedScrollEvent(entry.target);
                window.addEventListener('scroll', scrollEvent);
                inviewEvent.set(entry.target,scrollEvent);

表示領域内に入ったら、関数“inviewedScrollEvent”を発火させます。
“inviewedScrollEvent”は2回しようされます。1回目はIntersectionObserverのコールバックとして、要素が初めて表示領域に入ったとき(ページ読み込み時など)に呼び出されます。
2回目はスクロールイベントのコールバックとして使用され、スクロール中に要素の状態を監視し続けます。

    window.addEventListener('scroll', scrollEvent);
    inviewEvent.set(entry.target,scrollEvent);

スクロールイベントの発火と、Mapオブジェクトに格納しています。
Mapインベントで動作中のスクロールイベントを監視することで、多重にイベントを動作しないようにしています。

    if(!inviewEvent.has(entry.target) && !entry.target.classList.contains('is-inviewed')) {

こちらで、スクロールイベントを何回も呼び出さないよう、条件をかけています。
Mapオブジェクトに格納されていない(動作中でない)、かつ、アニメーションが終わっていない場合、スクロールイベントを発火させます。


    const scrollListener = inviewEvent.get(e);
    window.removeEventListener('scroll', scrollListener);

Mapオブジェクトから、要素eに関連付けられたスクロールイベントリスナーを取得し、スクロールイベントリスナーをwindowから削除しています。
これによりインビューアニメーションが終わったスクロールイベントリスナーが除去されます。

    inviewEvent.delete(e);

Mapオブジェクトから、インビューアニメーションが終わった要素を除去します。


    if(wh * .5 <= scrollAmount && elementEnd >= scrollAmount) {

この条件分岐で、画面内の表示位置を条件分岐させています。
例えば、画面内25%に表示されたら動作させる場合、以下のようになります。

    if(wh * .25 <= scrollAmount && elementEnd >= scrollAmount) {

    if(scrollAmount <= 0 && !e.classList.contains('is-inviewed')) {
        const scrollListener = inviewEvent.get(e);
        window.removeEventListener('scroll', scrollListener);
        inviewEvent.delete(e);
    }

スクロールイベントを動作させている状態で、画面外にでた場合はスクロールイベントリスナーを除去します。
これにより、不要なスクロールイベントを行わなくなります。

スクロールイベントを使わずに"threshold"の値を動的に取得する方法

これまでスクロールイベントを使った方法について説明しましたが、"threshold"の値を適切に計算することで、スクロールイベントに頼らず、インビューアニメーションを正確なタイミングで動作させることができます。

"threshold”の数値は、ビューポート内に表示された高さ(インビューアニメーションを開始するタイミング) / 監視対象要素の高さで求めることができます。

例えば、次のような条件で考えてみましょう:
- ビューポートの高さ: 1000px
- コンテンツの高さ: 3000px
- ビューポートの50%の高さでインビューアニメを開始したい

この場合、ビューポート内に表示させるタイミングは、1000px × 0.5(50%)=500pxになります。
そのため"threshold”の値は、500px / 3000px = 0.1667 となります。
つまり、「threshold: 0.1667(丸めて0.17)」とオプションに設定することで、画面の中央付近でインビューアニメーションを開始させることができます。

ただし、この方法を使用する場合、リサイズイベントやコンテンツの高さが動的に変わるケース(例えば、アコーディオンメニューの展開)に対応する必要があります。
これらのケースでは、`IntersectionObserver`の設定を再計算し、更新する処理を取り入れることを検討しましょう。

まとめ

いかでしたでしょうか?
今回の記事では、交差オブザーバーAPI(Intersection Observer API)を活用し、パフォーマンスを向上させたインビューアニメーションについて解説をしました。

今回の記事のまとめ

  • 交差オブザーバーAPI(Intersection Observer API)単体でできなかった理由
  • 交差オブザーバーAPI(Intersection Observer API)とスクロールイベントの組み合わせ解説

交差オブザーバーAPI(Intersection Observer API)はとても便利で簡単に導入することができます。
スクロールイベントと組み合わせたり、他のイベントと合わせることでさらに自由度の高い動作を実現することができます。
特定の条件が満たされたときに追加のアクションを実行することができ、この柔軟性により、ユーザー体験の向上やパフォーマンスの最適化を行えます。

今回の記事は私自身の備忘録としての側面が大きいですが、参考になれば幸いです。

最後までお読みいただきありがとうございました!

免責事項

  • 当ブログでは、執筆者の経験に基づいた技術情報や知識を提供していますが、その正確性や普遍性を保証するものではありません。情報は執筆時点のものであり、技術の進展により古くなる可能性があります。これらの情報を利用する際は、自己責任で行ってください。必要に応じて専門家の助言を求めることをお勧めします。
  • 当ブログで提供するプログラムコードは、執筆者の最善の知識に基づいていますが、その正確性や完全性を保証するものではありません。コードの利用や実行により生じた損害や問題については、一切の責任を負いかねます。コードの使用は、自己責任で行ってください。
  • 当サイトで使用しているスクリーンショット画像について、著作権はサイトの権利者に帰属します。掲載に不都合がある場合、お手数ですがお問い合わせフォームよりご連絡ください。
  • 当サイトからリンクよって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いかねますのでご了承ください。
  • 当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。