🗂

【JS】Intersection Observerでスクロールとナビを連動させてみよう

2023/05/26に公開

概要

現在、とある案件でナビをクリックするとページ内リンクでスクロールするようにしているが、普通にスクロールしてアンカーリンクのコンテンツが表示されたらナビゲーションも連動して背景色を変えてみようっていう仕組み。

今回のDEMOはこちら

これまではscrollイベントが主流

これまでは下記のようなscrollイベントでの設定が一般的だった。


window.addEventListener("scroll", () => {
  const sroll = window.pageYOffset;
  if (srollVal > contentsPosition) {
     // フェードアニメーションを呼び出す
    fadeAnime();
  }
});

// スクロールイベント
$(window).scroll(function() {
    // フェードアニメーションを呼び出す
    let scroll = $(window).scrollTop();
    fadeAnime();
  });

ただこの方法だと、スクロールがいつも発生しているから場合によって処理が重くなる。

Intersection Observer API

そこで代わりになりそうなIntersection Observer APIというのがある。
詳細は以下のサイトで説明されてるが、一部引用すると下記のように要素が交差した時にコールバック関数が呼び出されるので、最適化することができる。

交差オブザーバー API では、監視したい要素が他の要素(またはビューポート)に入ったり出たりしたとき、あるいは両者が交差する量が要求された量だけ変化したときに実行されるコールバック関数をコードに登録することができます。この方法により、サイトはこの種の要素の交差を監視するためにメインスレッドで何もする必要がなくなり、ブラウザーは自由に交差の管理を最適化することができます。

https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

実装

実際にDEMOのように実装する内容は以下。
よくあるナビゲーションをクリックするとスムーズスクロールでページ内リンクするページとする。

スクロールしてコンテンツがウィンドウの中央あたりに表示、、つまり交差したら該当するナビゲーションにactiveクラスを付与して背景色を設定して現在を示す。
またせっかくなので画像の切り替えもやってみるが仕組みはナビゲーションと同様activeを付与する。

CSSに関しては見た目の話なので割愛する。

html


<!--ナビゲーション-->
<nav id="js-nav" class="nav">
        <ul class="nav__list">
            <li><a href="#content1">コンテンツ1</a></li>
            <li><a href="#content2">コンテンツ2</a></li>
            <li><a href="#content3">コンテンツ3</a></li>
            <li><a href="#content4">コンテンツ4</a></li>
        </ul>
</nav>

<!--切り替え用の画像-->
 <div id="js-photo" class="photo">
        <p>写真をフェードイン</p>
        <div class="inner">
            <img class="photo__img content1" src="./images/photo1.jpg" alt="">
            <img class="photo__img content2" src="./images/photo2.jpg" alt="">
            <img class="photo__img content3" src="./images/photo3.jpg" alt="">
            <img class="photo__img content4" src="./images/photo4.jpg" alt="">
        </div>
  </div>

<!--監視対象のターゲット-->
  <article class="scroll">
        <div id="content1" class="scroll__target">
            <h1>コンテンツ1</h1>
            <p>このブロックがスクロールのターゲットとなる。</p>
        </div>

        <div id="content2" class="scroll__target">
            <h1>コンテンツ2</h1>
            <p>このブロックがスクロールのターゲットとなる。</p>
        </div>

        <div id="content3" class="scroll__target">
            <h1>コンテンツ3</h1>
            <p>このブロックがスクロールのターゲットとなる。</p>
        </div>

        <div id="content4" class="scroll__target">
            <h1>コンテンツ4</h1>
            <p>このブロックがスクロールのターゲットとなる。</p>
        </div>
  </article>

idとclassを設定する

スクロールの監視対象とするスクロールのコンテンツにはscroll__targetというクラスを付与する。またアンカーリンクのidを設定しておく。

ナビゲーションと画像にはアンカーリンクのidをクラス名として設定しておく。
こちらのクラスを名でどのコンテンツと連動させるか決めている。

JavaScript

実際のJavaScriptのコードは以下。
Intersection Observerは交差した際の処理以外は決まり文句のような書き方と感じるので、余計な処理を省いたソースコードを見たほうが理解できるかも。

Intersection Observerの全体像

まずは、Intersection Observerの全体像として以下のようにテンプレのような感じ。


//コールバック関数
const callback = (entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            //要素が交差した
        } else {
            //交差から外れた
        }
    })
}

//オプション
const options = { rootMargin: '-50% 0px' }

//IntersectionObserverのインスタンス生成
let observer = new IntersectionObserver(callback, options)

//監視要素を取得して監視登録
let targets = document.querySelectorAll('.scroll__target');
targets.forEach(target => {
    observer.observe(target);
});

コールバック関数

IntersectionObserverのインスタンスの引数にはコールバック関数とオプションを指定できる。
実際に監視対象の要素はentriesに入っているので、isIntersectingで交差したかどうかをチェックできる。
trueの場合は交差しているので、交差した場合の処理、、今回でいえばactiveクラスの付与という処理となる。(後述)

オプション

オプションは省略しているがrootというオプションで交差する要素を指定できる
rootに要素を指定すると監視対象の領域とすることができます。
デフォルトはブラウザのビューポートがでnullと指定できるが、ビューポートの場合は省略ができる。

rootMarginは交差する領域を指定する。
例えばウィンドウの中央に設定したい場合は以下のように"-50%"を指定すると中央になる。
なかなか理解しずらいので参考サイトを見てほしい。

const options = { 
root:document.querySelector('.wrapper'),
rootMargin: '-50% 0px'
}

https://www.webdesignleaves.com/pr/jquery/intersectionObserverAPI-basic.html

IntersectionObserverのインスタンス生成

IntersectionObserverのインスタンス生成して引数にコールバック関数とオプションを指定する。

監視要素を取得してobserveに登録する

一番最後にquerySelectorAllで監視対象の要素を取得して、ループでIntersectionObserverのインスタンスにあるobserveメソッドに登録する。

実際のソースコード

上記を踏まえて上で実際のコード。
IntersectionObserverの説明してるので、交差した際の処理。

//コールバック関数
const callback = (entries) => {
   entries.forEach(entry => {
      if (entry.isIntersecting) {
               
       //すでにアクティブになっているものが0個の時(=null)以外は、activeクラスを除去
        const currentActiveImg = document.querySelector("#js-photo .active");
        if (currentActiveImg !== null) {
            currentActiveImg.classList.remove("active");
          }

        let targetId = entry.target.id;
        let imgElement = document.querySelector(`.${targetId}`);
        
	if (imgElement) {
            imgElement.classList.add("active");
         }

      // すでにアクティブになっているものが0個の時(=null)以外は、activeクラスを除去
       const currentActiveIndex = document.querySelector("#js-nav .active");
       if (currentActiveIndex !== null) {
           currentActiveIndex.classList.remove("active");
         }

      // 引数で渡されたDOMが飛び先のaタグを選択し、activeクラスを付与
       const newActiveIndex = document.querySelector(`#js-nav a[href='#${entry.target.id}']`);
         newActiveIndex.classList.add("active");
            } else {
          
	  if (entry.target.id === 'content1') {
             let getActiveIndex = document.querySelector("#js-nav .active");
             let getActiveImg = document.querySelector("#js-photo .active");

             if (getActiveIndex !== null) {
                 //active削除
                 getActiveIndex.classList.remove("active");
               }
         }
    });
}

//オプション
const options = {
     root: null, // ビューポートをルートとする
     rootMargin: "-50% 0px", // ビューポートの中心を判定基準にする
 }

// IntersectionObserverオブジェクトを作成
let observer = new IntersectionObserver(callback, options);

// 監視したい要素を取得
let targets = document.querySelectorAll('.scroll__target');

targets.forEach(target => {

if (target) {
   observer.observe(target);
   } else {
   console.log('target element not found');
 }
});

すでにアクティブになっているものがある場合は、activeクラスを除去

ソースコードを抜粋。
画像とナビゲーションにactiveクラスの削除と付与は同じロジック。

 const currentActiveImg = document.querySelector("#js-photo .active");
    
   if (currentActiveImg !== null) {
       currentActiveImg.classList.remove("active");
      }

基本的にactiveクラスは1つしかついてないはずなので、qureySelectoractiveクラスがある要素を取得。
もし、存在するのであればクラスを削除。

 let targetId = entry.target.id;
     let imgElement = document.querySelector(`.${targetId}`);
     if (imgElement) {
           imgElement.classList.add("active");
        }

そのあとに監視しているコンテンツ要素からidを取得したいので、entry.target.idでid属性にアクセスできるので、画像要素にもid名と同じクラス名を設定しているので、querySelectorで取得して、activeクラスを設定する。

ナビゲーションの場合はhref属性にid名と同じ#content1などアンカー名を設定している要素をquerySelectorで取得してactiveクラスを付与する。

// 引数で渡されたDOMが飛び先のaタグを選択し、activeクラスを付与
const newActiveIndex = document.querySelector(`#js-nav a[href='#${entry.target.id}']`);

newActiveIndex.classList.add("active");

監視している領域から外れた際の処理について

基本的に監視対象に入った際の処理でまずはactiveクラスを持っている要素からactiveクラスを削除しているが、なぜか最初のコンテンツ要素がスクロールダウン(ページ上部に戻る)で交差対象から外れた際にクラスが外れなかったので以下の処理を追加した。

if (entry.target.id === 'content1') {
       let getActiveIndex = document.querySelector("#js-nav .active");
       let getActiveImg = document.querySelector("#js-photo .active");

       if (getActiveIndex !== null) {
          //active削除
          getActiveIndex.classList.remove("active");
         }
 }

すごく限定的だが、一番最初に交差するコンテンツのidがcontent1なので交差から外れたコンテンツのidをentry.target.idで取得して、一番最初の要素がもしactiveクラスを持っていたら、削除するという処理。

まとめ

これまで、scrollイベントで処理していたが、細かい交差する境界線の設定はscrollイベントで各要素のtop要素などを取得したほうが便利そうかも。
ただ総合的に、処理速度はこちらのほうがいいかも。

以下のサイトを参考にしました。

https://www.webdesignleaves.com/pr/jquery/intersectionObserverAPI-basic.html
https://www.webcreatorbox.com/tech/intersection-observer
https://ics.media/entry/190902/

Discussion