Intersection Observer API の使い方

Intersection Observer API は特定の領域を監視して指定した要素がその領域に入ったかどうかを検知してくれる JavaScript の API です。

例えば、スクロールして要素が画面上に現れた時にアニメーションを開始したり、スクロールスパイ(要素が表示されている位置に対応するリンクをハイライトする)などの機能を比較的簡単に実装できます。

スクロールを検出する方法とは異なり、パフォーマンス的にも優れていて、主なモダンブラウザで利用することができます。can i use IntersectionObserver

IE など古いブラウザに対応するには polyfill が必要です。

作成日:2022年5月4日

Intersection Observer API 概要

Intersection Observer は直訳すると「交差観察者」というような意味になりますが、Intersection Observer API は特定の領域を監視して、監視対象の要素がその領域に交差したかどうか、言い換えると監視対象の要素がその領域に入ったかどうかを検知してくれます。

そして交差したことを検知すると(交差状態を検知して)、コールバック関数を呼び出します。

監視する領域やコールバック関数を呼び出すタイミングはオプションで指定することができます。また、監視対象の要素は複数指定することもできます。

監視する領域(デフォルトはブラウザ画面) 対象が領域内に見えている = 交差していると判定 交差している部分 監視対象の要素 コンストラクターで生成 オブザーバー 交差を検知 コールバック関数を実行

Intesection Observer API を利用するには監視する機能を提供する IntersectionObserver オブジェクト(オブザーバー)を生成し、その生成したオブジェクトに監視対象の要素を指定して監視させます。

IntersectionObserver (オブザーバー)を生成

IntersectionObserver オブジェクトは、コンストラクター IntersectionObserver() にコールバック関数(callback)とオプション(options)を指定して生成します。

IntersectionObserver( ) は生成したオブジェクト(オブザーバー)を返します

コンストラクター IntersectionObserver() の構文(options は省略可能)
const observer = new IntersectionObserver( callback [, options] );

オプションには監視する領域やコールバック関数を呼び出すタイミングなどを指定することができます。

オプションを省略した場合は、監視する領域(root)はブラウザのビューポート(ブラウザの画面内に表示されている領域)になり、コールバック関数を呼び出すタイミング(threshold)は交差を検知したタイミング(領域に対象の要素が出入りする際)になります。

observe() で要素を監視

要素を監視するには、生成したオブジェクトの observe() メソッドに監視対象の要素を指定します。

//監視する対象の要素を取得(直接 observe() メソッドの引数に指定可能)
const targetElem = document.querySelector('.targetElem');
  
//対象の要素をオブジェクトの observe() メソッドに指定
observer.observe(targetElem);

コールバック関数の定義

コールバック関数を定義してコンストラクタに渡すと、そのコールバック関数はオプションの threshold に指定したタイミングで呼び出されます。

threshold は省略可能で、省略した場合はオブザーバーが交差を検知したタイミングで呼び出されます。

コールバック関数は、引数に IntersectionObserverEntry オブジェクトの配列(以下では entries)を受け取るので、forEach() などを使って各要素(以下では entry)に対して処理を行います。

IntersectionObserverEntry オブジェクトは、監視する領域と監視対象要素の「交差状態を表すオブジェクト」で、その target プロパティ(entry.target)で監視対象の要素にアクセスすることができます。

監視する対象の要素が1つだけの場合でも配列が渡されます。もし、監視対象の要素が1つだけであれば、forEach() は使わずに entries[0] でオブジェクトにアクセスして、entries[0].target で監視対象の要素にアクセスできます。

また、isIntersecting プロパティ(entry.isIntersecting)は、監視対象の要素が指定した領域に入っているかどうか(交差しているかどうか)を表す真偽値を返します(詳細:isIntersecting)。

//コールバック関数
const callback = (entries) => {
  //監視対象の要素には entry.target でアクセス
  entries.forEach( (entry) => {
    //entry.isIntersecting の値で要素が領域内にあるかどうかを判定可能
  });
}

例えば、ブラウザの画面を監視領域として、以下の要素を監視する場合、

<div id="targetElem">監視対象の要素</div>

以下を記述すると、上記の要素が画面に見えている場合は、コンソールに「監視対象の要素が見えています」と出力され、画面に見えない場合は「監視対象の要素は見えていません」と表示されます。

//コールバック関数  
const callback = (entries) => {
  entries.forEach( (entry) => {
    //監視対象の要素が指定した領域に入っている(見えている)場合
    if(entry.isIntersecting) {
      //監視対象の要素(entry.target)の textContent を使ってメッセージを出力
      console.log(entry.target.textContent + 'が見えています');     
    }else{
      console.log(entry.target.textContent + 'は見えていません');
    }
  });
}

//IntersectionObserver オブジェクト(オブザーバー)を生成  
const observer = new IntersectionObserver(callback);
  
//要素を監視
observer.observe(document.getElementById('targetElem'));

この例のように1つの要素だけを監視する場合であれば、コールバック関数は以下のように記述することもできます。

//コールバック関数
const callback = (entries) => {
  //配列の最初の要素 entries[0]
  if (entries[0].isIntersecting) {
    console.log(entries[0].target.textContent + 'が見えています');     
  }else{
    console.log(entries[0].target.textContent + 'は見えていません');
  }
}

また、コールバック関数を別途定義せずに、以下のように直接コンストラクタに渡すこともできます。

//オブザーバーを生成  
const observer = new IntersectionObserver( (entries) => {
  if (entries[0].isIntersecting) {
    console.log(entries[0].target.textContent + 'が見えています');     
  }else{
    console.log(entries[0].target.textContent + 'は見えていません');
  }
});
  
//要素を監視
observer.observe(document.getElementById('targetElem'));

動作確認サンプル 1

以下のような HTML で .wrapper を監視する領域として、.targetElem を監視対象の要素する場合、

<!--監視対象の領域 -->
<div class="wrapper">
  ・・・その他の要素・・・
  <div class="targetElem">監視対象の要素</div>
  ・・・その他の要素・・・
</div>

オプションの root に監視対象の領域(.wrapper)を指定してオブザーバーを生成し、その observe() メソッドに監視対象の要素(.targetElem)を指定して監視します。

オブザーバーが交差を検知してコールバック関数が呼び出されると、その時点での isIntersecting プロパティ(要素が指定した領域に入ったかどうかを表す真偽値)の値を出力します。

//isIntersecting の値の出力先の span 要素
const status =  document.querySelector('#status span');

//オプション  
const options = {
  //監視対象の領域(wrapper クラスを指定した要素)を指定
  root: document.querySelector('.wrapper'),
}
 
//コールバック関数
const callback = (entries) => {
  //entry は IntersectionObserverEntry オブジェクト
  entries.forEach( entry => {
    //isIntersecting プロパティの値を出力
    status.textContent = entry.isIntersecting;
    //isIntersecting が true の場合(対象が領域内の場合)
    if(entry.isIntersecting) {
      status.style.color = 'blue';
    }else{
      //isIntersecting が false の場合(対象が領域外の場合)
      status.style.color = 'red';
    }
  });
}
 
//IntersectionObserver オブジェクト(オブザーバー)を生成
const observer = new IntersectionObserver(callback, options);
 
//監視する対象の要素(targetElem クラスを指定した要素)を取得
const targetElem = document.querySelector('.targetElem');
  
//対象の要素を observe() メソッドに指定して監視
observer.observe(targetElem);

以下の枠線の部分が監視対象の領域(wrapper クラスの div 要素 = root)です。

この例では threshold オプションを指定していないので、オブザーバーが交差を検知したタイミング(対象要素が領域内に見えた時と見えなくなった時)でコールバック関数が呼び出されます。

この例の初期状態では、監視する領域の可視範囲内に監視対象の要素がない(交差していない)ので、isIntersecting の値は false になっています。

isIntersecting の値:

監視対象の要素

ボックス内でスクロールして対象の要素が現れるとボックス上の isIntersecting の値が変わります。

<p id="status">isIntersecting の値: <span></span></p>
<div class="wrapper"><!--監視対象の領域-->
  <div class="sample"></div><!--高さを指定しただけの要素-->
  <div class="targetElem">監視対象の要素</div>
  <div class="sample"></div><!--高さを指定しただけの要素-->
</div>
/*監視対象の領域*/
.wrapper { 
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;  /*overflow に scroll を指定*/
}
/*前後の空白を作成するために高さだけを指定した要素*/
.sample {
  height: 300px;
}
/*監視対象の要素*/
.targetElem {
  width: 200px;
  height: 160px;
  background-color: darkseagreen;
  margin: 0 auto;
  text-align: center;
  line-height: 160px;
  color: #fff;
} 

動作確認サンプル 2

以下はオプションの root は指定せずに、監視する領域をブラウザのビューポート(ブラウザの画面に表示されているエリア全体)として、対象の要素が見えたらクラスを追加してアニメーション表示する例です。

HTML
<div class="target"><!--監視対象の要素-->
  <p>Hello there!</p>
</div>  

監視対象の要素とその子要素にはトランジションを設定して、active クラスの着脱でアニメーション表示するようにしています。

CSS
/*監視対象の要素*/
.target {
  width: 200px;
  height: 100px;
  background-color: cornflowerblue;
  /*2秒で背景色を変更するトランジションを設定(遅延1秒)*/
  transition: background-color 2s 1s; 
} 
/*active クラスが追加されたら背景色を変更*/
.target.active {
  background-color: palevioletred;
}
/*監視対象の要素の子要素*/
.target p {
  width: 100%;;
  color: white;
  margin: 0 auto;
  text-align: center;
  line-height: 100px;
  font-size: 20px;
  opacity: 0;
  /*2秒で不透明度を変更するトランジションを設定*/
  transition: opacity 2s; 
} 
/*active クラスが追加されたら不透明度を変更*/
.target.active p {
  opacity: 1;
}

この例ではオプションの rootMargin に '-50px 0px' と指定して、対象の要素が 50px 見えたらコールバック関数を呼び出すようにしています。

isIntersecting の値を判定して、要素が領域に入ったら active クラスを追加し、領域外になったら active クラスを削除しています(※ rootMargin を指定しているので判定は 50px ずれます)。

//オプション  
const options = {
  //50px 見えてからコールバック関数を呼び出す
  rootMargin: '-50px 0px'
}
 
//コールバック関数
const callback = (entries) => {
  entries.forEach( (entry) => {
    //監視対象の要素が領域内に入った場合の処理
    if (entry.isIntersecting) {
      //監視対象の要素(entry の target プロパティ)に active クラスを追加
      entry.target.classList.add('active');
    } else { //監視対象の要素が領域外になった場合の処理
      //監視対象要素から active クラスを削除
      entry.target.classList.remove('active');
    }
  });
}
 
//IntersectionObserver オブジェクト(オブザーバー)を生成
const observer = new IntersectionObserver(callback, options);
 
//監視する対象の要素(target クラスを指定した要素)を取得
const target = document.querySelector('.target');
  
//対象の要素を observe() メソッドに指定して監視
observer.observe(target);

要素が領域(ビューポート)内に入って 50px スクロールしたら active クラスを追加し、領域外に出る手前 50px で active クラスを削除するので、要素が可視範囲に現れたら毎回アニメーションが実行されます。

Hello there!

サンプル

以下は Intersection Observer API を使ったサンプルです。

Intersection Observer API の詳細は以降のセクションを御覧ください。

要素が画面内に入ったらアニメーションを開始

特定の要素が画面上に見えたときにアニメーションを開始するサンプルです。

この例では target クラスを指定した要素を監視して、その要素が見えたらコールバック関数でアニメーションを開始(トリガー)するためのクラスを追加します。

対象の要素の子要素には実行するアニメーションのクラス(fadeInFromRight など)を付与しておきます。

HTML(動作を確認するには前後にある程度の高さがある要素が必要です)
<div class="target">
  <div class="fadeInFromRight rect">target.rect</div>
</div>
<div class="target">
  <div class="fadeInFromLeft circle">target.rect</div>
</div>
<div class="target">
  <div class="rotateFromBottom square">target.square</div>
</div>

CSS では .target.inView .animationClass(.animationClass は fadeInFromRight などのアニメーションのクラス)とすることで、監視対象の要素 .target.inView が追加されると、その子要素は設定されたアニメーションが適用されます。

この例ではキーフレームアニメーションを設定していますが、「動作確認サンプル 2」のようにトランジションを設定することもできます。

※ 以下のキーフレームアニメーションでは移動(transform)と不透明度(opacity)のアニメーションを別々に定義して、両方を指定していますが、この場合、Safari ではアニメーション終了後、フェードインが発生します。移動(transform)と不透明度(opacity)を一緒に設定した場合は、そのような現象は起きませんが、今度は他のブラウザでスクロールのスピードによってはアニメーション開始時の動きが少しおかしくなります。

この例では関数 setObserver() を定義し、その中でオブザーバーを設定しています。このようにすることで、定義する変数名(例えば observer や callback など)等の競合をあまり気にしないですみます。

関数にせずに、4〜35行目までをそのまま記述することもできます。

コールバック関数では、isIntersecting が true の場合に対象の要素に inView クラスを追加し、false の場合は削除することでアニメーションを実行するようにしています。

オプションの threshold(コールバック関数を呼び出すタイミングの閾値)にはを [0,1] を指定しています。これにより、対象が見えた時に呼び出される際には inView クラスを追加し、要素が完全に見えなくなった時に呼び出される際には inView クラスを削除するようにしています。

オブザーバーは複数の要素を監視することができます。この例では querySelectorAll() で監視対象の要素を全て取得して、取得した要素の集まり(NodeList)の forEach() メソッドを使って各要素を observe() メソッドに指定しています。

//IntersectionObserver を設定する関数
const setObserver = () => {
  
  //コールバック関数の定義
  const callback = (entries) => {
    //各 entry(IntersectionObserverEntry オブジェクト)に対して
    entries.forEach( (entry) => {
      //監視対象の要素が領域内に入った場合の処理
      if (entry.isIntersecting) {
        //監視対象の要素(entry.target)に inView クラスを追加
        entry.target.classList.add('inView');
      } else { //監視対象の要素が領域外になった場合の処理
        //監視対象要素から inView クラスを削除
        entry.target.classList.remove('inView');
      }
    }); 
  }
  
  //オプション
  const options = {
    //コールバック関数を呼び出すタイミングを指定
    threshold: [0,1]
  }

  //引数にコールバック関数とオプションを指定してオブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 

  //監視対象の要素(target クラスを指定した要素)を全て取得
  const targets = document.querySelectorAll('.target');

  //全ての監視対象要素を observe() メソッドに指定
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });  
}
//上記関数の実行
setObserver();  

※ この例の場合、監視対象の要素とアニメーションを適用する要素(この場合は子要素)は異なるようにしています。translateY() を使った垂直方向のアニメーションなどの場合、監視対象とアニメーション適用を同じ要素にすると、監視対象が垂直方向(スクロールする方向)に動いてしまうのでスクロールの位置によっては繰り返しアニメーションが実行されてしまう場合があります。

動作確認サンプル 2 」のように、監視対象の要素とアニメーションを適用する要素を同じ要素にしても通常は問題ないと思いますが、分けると管理しやすい場合があります。

また、この例では関数の定義の後で関数を呼び出して実行していますが、DOMContentLoadedload イベントを使って呼び出すこともできます。

document.addEventListener('DOMContentLoaded', () => {
  //DOM ツリーの構築(解析)が完了した時点で呼び出す
  setObserver(); 
}); 
target.rect
target.circle
target.square

以下は対象の要素に p 要素を追加した場合の例です。この例では対象の要素の範囲がわかりやすいように背景色を適用しています。

<div class="target">
  <div class="fadeInFromRight rect">target.rect</div>
  <p>Lorem ipsum dolor sit amet,...debitis.</p>
  <p>Repellendus in...voluptatibus possimus!</p>
  <p>Inventore sequi nobis, ...quasi, nulla.</p>
</div>
<div class="target">
  <div class="fadeInFromLeft circle">target.circle</div>
  <p>Lorem ipsum dolor ... tempore et pariatur quia.</p>
  <p>Numquam eius ...nam ipsa, laborum.</p>
  <p>Doloremque aperiam ... autem iure numquam.</p>
</div>
<div class="target">
  <div class="rotateFromBottom square">target.square</div>
  <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
  <p>Iste, nemo, ... Inventore quos harum beatae.</p>
  <p>Sint magnam pariatur... possimus voluptate.</p>
</div>
target.rect

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, est, qui, magnam dolor tenetur voluptate ab ipsam ad voluptatum iusto beatae obcaecati laudantium quisquam debitis dolore accusamus vitae hic recusandae.

Iste, nemo, labore quidem quaerat error dolor consequatur modi expedita praesentium voluptatem eum dolorum! Cupiditate, assumenda, enim! Molestias nemo quaerat earum doloremque deserunt error porro aspernatur! Inventore quos harum beatae.

Sint magnam pariatur, facere unde eaque aperiam nobis quod quidem nesciunt earum, recusandae, voluptatibus ab modi nostrum, omnis repellat placeat! Similique ut recusandae magnam modi nobis reprehenderit, aspernatur possimus voluptate.

target.circle

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Veritatis eligendi omnis in, animi maxime perferendis voluptatibus eveniet nulla hic molestias voluptates atque exercitationem voluptatem delectus ullam, tempore et pariatur quia.

Numquam eius veritatis minus asperiores, quidem dolorum ullam. Odio eveniet aperiam tenetur distinctio harum est libero dolor iusto, quibusdam autem eum sequi natus perspiciatis reprehenderit. Impedit suscipit nam ipsa, laborum.

Doloremque aperiam pariatur ab assumenda libero beatae quidem laboriosam odio deleniti asperiores? Cupiditate in enim aliquam quas eum officiis, sint ex vel sapiente praesentium, iste quaerat blanditiis autem iure numquam.

target.square

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, est, qui, magnam dolor tenetur voluptate ab ipsam ad voluptatum iusto beatae obcaecati laudantium quisquam debitis dolore accusamus vitae hic recusandae.

Iste, nemo, labore quidem quaerat error dolor consequatur modi expedita praesentium voluptatem eum dolorum! Cupiditate, assumenda, enim! Molestias nemo quaerat earum doloremque deserunt error porro aspernatur! Inventore quos harum beatae.

Sint magnam pariatur, facere unde eaque aperiam nobis quod quidem nesciunt earum, recusandae, voluptatibus ab modi nostrum, omnis repellat placeat! Similique ut recusandae magnam modi nobis reprehenderit, aspernatur possimus voluptate.

アニメーションを適用する要素

前述の例や「動作確認サンプル 2 」では監視対象の要素やその子要素にアニメーションを適用しましたが、任意の要素にアニメーションを適用することができます。

いろいろな方法があると思いますが、以下では監視対象の要素とアニメーションを適用する要素に分けてそれら全体を div 要素で囲む以下のような構造の HTML にします。

全体を囲む div 要素には必要に応じて任意のクラスを指定するとよいと思いますが、この例では何も指定していません。

また、以下のようなクラスを指定します。.transXFadeIn や .blueRect はどのようなアニメーションにするかやその要素のスタイルにより異なるクラスを指定することになります。

  • .target : 監視対象の要素に指定するクラス(必須)
  • .animTarget : アニメーションを適用する要素に指定するクラス(必須)
  • .transXFadeIn : アニメーションを定義してあるクラス(アニメーションにより異なる)
  • .blueRect : アニメーションを適用する要素のスタイル用のクラス(スタイルにより異なる)
<div><!--必ず全体を div 要素で囲む -->
  <div class="animTarget transXFadeIn blueRect"><!--アニメーションを適用する要素 -->
    <h3>animTarget H3</h3>
  </div>
  <div class="target"><!--監視対象の要素 -->
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>

アニメーションを適用する要素は、監視対象の要素(entry.target)の親要素(全体を囲む div 要素)を parentElement で取得して、その親要素から querySelector() で .animTarget の要素を取得します。

また、この例では threshold に 0.2 を指定して、監視対象の要素の2割が見えたらアニメーション開始するようにしています(25行目)。

その他は前述の例と同じです。

//IntersectionObserver を設定する関数
const setAnimationObserver = () => {
  
  //コールバック関数の定義
  const callback = (entries) => {
    //各 entry(IntersectionObserverEntry オブジェクト)に対して
    entries.forEach( (entry) => {
      
      //アニメーションを適用する要素(監視対象の親要素から.animTarget を取得)
      const animTarget = entry.target.parentElement.querySelector('.animTarget')
      
      //監視対象の要素が領域内に入った場合の処理
      if (entry.isIntersecting) {
        //アニメーションを適用する要素に inView クラスを追加
        animTarget.classList.add('inView');
      } else { //監視対象の要素が領域外になった場合の処理
        //アニメーションを適用する要素から inView クラスを削除
        animTarget.classList.remove('inView');
      } 
    }); 
  }
  
  //オプション
  const options = {
    //コールバック関数を呼び出すタイミングを指定
    threshold: 0.2
  }
 
  //引数にコールバック関数とオプションを指定してオブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
 
  //監視対象の要素(target クラスを指定した要素)を全て取得
  const targets = document.querySelectorAll('.target');
 
  //全ての監視対象要素を observe() メソッドに指定
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });  
}
//上記関数の実行
setAnimationObserver(); 
/*監視対象の要素に付与するクラス*/
.target {
  position: relative;  /*省略可能*/
}
/*アニメーションを適用する要素*/
.animTarget {
  opacity: 0; /*最初は非表示*/
}
/*アニメーションを適用する要素のスタイル*/
.blueRect {
  color: #fff;
  text-align: center;
  margin: 30px auto;
  width: 200px;
  height: 50px;
  line-height: 50px;
  background-color: cornflowerblue;
}
/* アニメーション(.inView はコールバック関数で追加).*/
.transXFadeIn.inView {
  animation: transXFadeIn .8s forwards;
}
/*キーフレーム*/
@keyframes transXFadeIn {
  0% {
    transform: translateX(900px);
    opacity: 0;
  }
  50% {
    transform: translateX(-600px);
    opacity: 0.5;
  }
  100% {
    transform: translateX(0px);
    opacity: 1;
  }
}

animTarget H3

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

Quibusdam iure dolor dolores quas, repudiandae voluptas error obcaecati nemo praesentium, qui enim aliquid molestiae ipsum deserunt! Ipsum dolorem assumenda et iure vero exercitationem molestiae illo eius, voluptatum minima blanditiis?

Hic impedit error, neque nisi quod temporibus. Ab facilis, consectetur! Adipisci voluptates optio molestias deserunt repudiandae natus facere quas laboriosam illo numquam, doloremque, rerum laudantium, ab, quasi in perspiciatis maiores!

Aspernatur expedita saepe cupiditate culpa perspiciatis, nihil iure sint totam officiis rem fugit alias nobis consequatur, nam iusto distinctio odio tenetur dolores dolorum cumque quas maxime quo necessitatibus. Ipsum, fugiat!

Voluptate nostrum, ad illum placeat nihil magni. Dignissimos et, nostrum quas iste soluta quod autem porro itaque eius repellat saepe, excepturi facere praesentium? Quisquam cupiditate illo a quod quia incidunt!

アニメーションを一度だけ実行

前述の例では対象要素が領域に出入りする度にアニメーションを実行しますが、アニメーションを一度だけ実行場合は、一度クラスを追加したら unobserve() メソッドを使って要素の監視を停止すれば、それ以降アニメーションは実行されません。

unobserve() は observer のメソッドなので、以下ではコールバック関数の第二引数に observer を受け取って9行目で呼び出して監視を停止しています。

//コールバック関数(第二引数に observer を受け取る)
const callback = (entries, observer) => {
  entries.forEach( (entry) => {
    //監視対象の要素が領域内に入った場合の処理
    if (entry.isIntersecting) {
      //アニメーションを適用する要素に inView クラスを追加
      animTarget.classList.add('inView');
      //unobserve() メソッドで各要素の監視を停止
      observer.unobserve(entry.target);
    } 
  }); 
}
Web Animation API animate()

以下は Web Animation API の animate() メソッドを使ったアニメーションの場合の例です。

監視対象を target クラスの要素として、その要素が画面に入ったら fadeInFromRight クラスを指定した要素をアニメーションで表示します。

<div class="target">
  <div class="fadeInFromRight rect">target.rect</div>
  <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
  <p>Iste, nemo, ... Inventore quos harum beatae.</p>
  <p>Sint magnam pariatur... possimus voluptate.</p>
</div>

CSS では要素のスタイルのみを設定します。

/*要素のスタイル*/ 
.rect {
  width: 200px;
  height: 50px;
  line-height: 50px;
  background-color: cornflowerblue;
  color: #fff;
  text-align: center;
  margin: 30px 0;
}

JavaScript でアニメーションと監視を設定します。1〜19行目は Web Animation API の animate() メソッドを使ったアニメーションの設定です。

オブザーバーのコールバック関数では、要素が画面に入ったら生成したアニメーション(Animation オブジェクト)の play() でアニメーションを再生し、画面から出たら cancel() でアニメーションを停止します。

要素が画面から出た際には、アニメーションの再生状態を確認して、再生状態であれば停止するようにしていますが、そのまま停止しても問題ないかと思います。

アニメーションの内容によっては cancel() による停止ではなく、pause() で一時停止することもできます。

この例では、オブザーバー生成のオプション rootMargin に '50px 0px' を指定して画面に見える少し前にアニメーションを開始するようにしています(opacity によるちらつきが気になるため)。

//アニメーションのキーフレーム 
const keyframes = [
  { transform: 'translateX(900px)', opacity: 0 },
  { transform: 'translateX(0px)', opacity: 1 },
];
 
//アニメーションのタイミングオプション
const timing = {
  duration: 500,
  easing: 'cubic-bezier(0.83, 0, 0.17, 1)',
  fill: 'forwards'
}

//アニメーション対象の要素
const animTarget = document.querySelector('.fadeInFromRight');
//animate() メソッドでアニメーション(Animation オブジェクト)を生成
const animation = animTarget.animate(keyframes, timing);
//アニメーションの自動再生を停止  
animation.cancel();
  
//コールバック関数
const callback = (entries) => {
  entries.forEach( (entry) => {
    if (entry.isIntersecting) {
      //アニメーションを再生
      animation.play();
    } else {
      //アニメーションが再生状態であれば
      if(animation.playState === 'running') {
        //アニメーションを停止(キャンセル)
        animation.cancel();
      }
    }
  }); 
}

//オブザーバー生成のオプション(50px 手前で交差と判定)
const options = {
  rootMargin: '50px 0px'
}
//オブザーバーを生成
const observer = new IntersectionObserver(callback, options);
//オブザーバーに監視対象を指定
observer.observe(document.querySelector('.target')); 

以下も animate() メソッドを使った例ですが、複数の要素に設定できるようにしています。また、引数にキーフレームやタイミングオプション、対象の要素を受け取るようにしています。

<div class="target1">
  <div class="fadeInFromRight rect">rect</div>
  <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
  <p>Iste, nemo, ... Inventore quos harum beatae.</p>
  <p>Sint magnam pariatur... possimus voluptate.</p>
</div>
<div class="target2">
  <div class="rotateFromRight square">square</div>
  <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
  <p>Iste, nemo, ... Inventore quos harum beatae.</p>
  <p>Sint magnam pariatur... possimus voluptate.</p>
</div>
<div class="target1">
  <div class="fadeInFromRight ellipse">ellipse</div>
  <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
  <p>Iste, nemo, ... Inventore quos harum beatae.</p>
  <p>Sint magnam pariatur... possimus voluptate.</p>
</div>

CSS では要素のスタイルのみを設定します。

/*要素のスタイル*/ 
.rect {
  width: 200px;
  height: 50px;
  line-height: 50px;
  background-color: cornflowerblue;
  color: #fff;
  text-align: center;
  margin: 30px 0;
}
.square {
  width: 100px;
  height: 100px;
  line-height: 100px;
  background-color: palevioletred;
  color: #fff;
  text-align: center;
  margin: 30px 0;
}
.ellipse {
  width: 150px;
  height: 80px;
  line-height: 80px;
  border-radius: 50%;
  background-color: darkseagreen;
  color: #fff;
  text-align: center;
  margin: 30px 0;
}

この例では、取得した監視対象の要素を配列に変換したものを変数 targetArray に格納しています。

また、アニメーション対象の要素に animate() で生成したアニメーションオブジェクトを変数 animations に格納しています。

オブザーバーのコールバック関数が呼び出された際は、entry.target が何番目の監視対象の要素か(インデックス)を targetArray.indexOf(entry.target) で取得して、対応するアニメーションオブジェクトのメソッドを呼び出しています。

entries における entry のインデックス(何番目の要素か)は要素(entry.target)のインデックスに対応しているわけではありません。

/*
以下の引数を指定します(異なるアニメーションであれば、異なる対象の要素を指定します)
keyframes:アニメーションのキーフレーム
timing:アニメーションのタイミングオプション
animTargetsSelector:アニメーション対象の要素のセレクタ
targetSelector:監視対象の要素のセレクタ
*/
  
const setWebAnimationIO = (keyframes, timing, animTargetsSelector, targetSelector) => {
  
  //アニメーション対象の要素(animTargetsSelector の要素)を全て取得
  const animTargets = document.querySelectorAll(animTargetsSelector);
  
  //監視対象の要素(target クラスを指定した要素)を全て取得
  const targets = document.querySelectorAll(targetSelector); 
  //取得した要素を配列に変換したもの
  const targetArray = Array.from(targets);
  
  //アニメーション対象と監視対象の要素が存在すれば
  if(animTargets.length > 0 && targets.length > 0) {
    //生成したアニメーションを格納する配列
    const animations = [];

    //全てのアニメーション対象からアニメーションを生成して配列に追加 
    animTargets.forEach( (target) => { 
      animations.push(target.animate(keyframes, timing));
    });  

    //コールバック関数の定義
    const callback = (entries) => {
      entries.forEach( (entry) => {
        //entry.target のインデクスを取得
        const index = targetArray.indexOf(entry.target);
        //生成したアニメーションの配列に対応するインデクスのアニメーションがあれば
        if(animations[index]) {
          if (entry.isIntersecting) {
            //アニメーションを再生
            animations[index].play();
          } else {
            //再生中であれば停止
            if(animations[index].playState === 'running') {
              animations[index].cancel();
            }
          }
        } 
      }); 
    }
    //オブザーバー生成のオプション(50px 手前で交差と判定)
    const options = {
      rootMargin: '50px 0px'
    }
    //オブザーバーを生成
    const observer = new IntersectionObserver(callback, options);
    //オブザーバーに監視対象を指定
    targets.forEach( (target) => { 
      observer.observe(target);
    });  
  }
}

//キーフレーム
const fadeInFromRight = [
  { transform: 'translateX(900px)', opacity: 0 },
  { transform: 'translateX(0px)', opacity: 1 },
];
 
//タイミングオプション
const timingFIFR = {
  duration: 500,
  easing: 'cubic-bezier(0.83, 0, 0.17, 1)',
  fill: 'forwards'
}
  
//上記キーフレームとタイミングを指定して関数を実行
setWebAnimationIO(fadeInFromRight, timingFIFR, '.fadeInFromRight', '.target1');  
  
//キーフレーム
const rotateFromRight = [
  { transform: 'translateX(900px)  rotate(720deg)', opacity: 0 },
  { transform: 'translateX(0px)  rotate(0deg)', opacity: 1 },
];  
//タイミングオプション 
const timingRFR = {
  duration: 700,
  easing: 'ease-in',
  fill: 'forwards',
  delay: -100
}
 
//上記キーフレームとタイミングを指定して関数を実行(前述の関数の実行とは異なる要素を指定)
setWebAnimationIO(rotateFromRight, timingRFR, '.rotateFromRight', '.target2'); 
rect

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, est, qui, magnam dolor tenetur voluptate ab ipsam ad voluptatum iusto beatae obcaecati laudantium quisquam debitis dolore accusamus vitae hic recusandae.

Iste, nemo, labore quidem quaerat error dolor consequatur modi expedita praesentium voluptatem eum dolorum! Cupiditate, assumenda, enim! Molestias nemo quaerat earum doloremque deserunt error porro aspernatur! Inventore quos harum beatae.

Sint magnam pariatur, facere unde eaque aperiam nobis quod quidem nesciunt earum, recusandae, voluptatibus ab modi nostrum, omnis repellat placeat! Similique ut recusandae magnam modi nobis reprehenderit, aspernatur possimus voluptate.

square

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, est, qui, magnam dolor tenetur voluptate ab ipsam ad voluptatum iusto beatae obcaecati laudantium quisquam debitis dolore accusamus vitae hic recusandae.

Iste, nemo, labore quidem quaerat error dolor consequatur modi expedita praesentium voluptatem eum dolorum! Cupiditate, assumenda, enim! Molestias nemo quaerat earum doloremque deserunt error porro aspernatur! Inventore quos harum beatae.

Sint magnam pariatur, facere unde eaque aperiam nobis quod quidem nesciunt earum, recusandae, voluptatibus ab modi nostrum, omnis repellat placeat! Similique ut recusandae magnam modi nobis reprehenderit, aspernatur possimus voluptate.

ellipse

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur, est, qui, magnam dolor tenetur voluptate ab ipsam ad voluptatum iusto beatae obcaecati laudantium quisquam debitis dolore accusamus vitae hic recusandae.

Iste, nemo, labore quidem quaerat error dolor consequatur modi expedita praesentium voluptatem eum dolorum! Cupiditate, assumenda, enim! Molestias nemo quaerat earum doloremque deserunt error porro aspernatur! Inventore quos harum beatae.

Sint magnam pariatur, facere unde eaque aperiam nobis quod quidem nesciunt earum, recusandae, voluptatibus ab modi nostrum, omnis repellat placeat! Similique ut recusandae magnam modi nobis reprehenderit, aspernatur possimus voluptate.

関連ページ:Web Animation API の使い方

対象全体が見えたら

対象の要素全体が見えたらアニメーションを開始するなど何らかの処理を行うこともできます。

以下は対象の要素が全て表示されたらアニメーションを開始する例です。

※ 但し、対象の要素の高さがブラウザの画面(監視する領域)の高さより大きい場合、アニメーションは開始されることはありません。

HTML(動作を確認するには前後にある程度の高さがある要素が必要です)
<div class="target"><!--対象の要素-->
  <p>.target <span id="span"></span></p>
</div>
/*要素のスタイル*/
.target {
  width: 300px;
  height: 200px;
  background-color: rebeccapurple;
  color: #fff;
  text-align: center;
  line-height: 200px;
}

/*背景色を変更するアニメーションのクラス*/
.allInView {
  animation: backgroundChange 9s infinite;
}
@keyframes backgroundChange {
  0% {
    background-color:rebeccapurple;
  }
  20% {
    background-color:crimson;
  }
  45% {
    background-color:darkcyan;
  }
  70% {
    background-color:darkkhaki;
  }
  90% {
    background-color:darksalmon;
  }
}

全体が表示された時を検知するにはオプションの threshold に 1 を指定します。

threshold に 1 を指定すると、対象の要素の交差の割合が 1、つまり全体が表示された時と、そうではなくなった時にコールバック関数が呼び出されます。

全体が表示されているかどうかは、コールバック関数が呼び出された際に、isIntersecting で判定できます。isIntersecting が true の場合は、対象の要素全体が表示されている状態にあり、false の場合は一部または全体が表示されていない状態と判定することができます。

また、必要に応じて rootMargin を指定することで全体が見えてからのオフセット(距離)を指定することができます。

//オプション
const options = {
  //全体が表示された時と全体が表示されなくなる時にコールバック関数が呼び出される
  threshold: 1,
  //必要に応じてオフセットを指定
  //rootMargin: '-50px 0px',
}

//コールバック関数で使用するテキストを出力する要素を取得
const span = document.getElementById('span');

//コールバック関数の定義
const callback = (entries) => {
  //各 entry に対して
  entries.forEach( (entry) => {
    //監視対象の要素が領域内に入った際(この場合は全体が見えた際)の処理
    if (entry.isIntersecting) {
      //監視対象の要素(entry.target)に allInView クラスを追加
      entry.target.classList.add('allInView');
      //span 要素にテキストを表示
      span.textContent = '.allInView';
    } else { //一部が見えなくなった(全体が表示されていない)際の処理
      //監視対象要素から allInView クラスを削除
      entry.target.classList.remove('allInView');
      //span 要素のテキストを空に
      span.textContent = '';
    }
  }); 
}
  
//オブザーバーを生成
const observer = new IntersectionObserver(callback, options);  
//監視対象の要素(target クラスを指定した要素)を全て取得
const targets = document.querySelectorAll('.target');

//全ての監視対象要素を observe() メソッドに指定
targets.forEach( (elem) => {
  //observe() に監視対象の要素を指定
  observer.observe(elem);
});  

.target

交差の割合(intersectionRatio)で判定

上記の例では、全体が見えたかどうかは isIntersecting で判定していますが、intersectionRatio(交差の割合)で判定することもできます。

以下は交差の割合 intersectionRatio が1以上かどうかで判定するコールバック関数の例です。動作は上記と同じになります。

const callback = (entries) => {
  entries.forEach( (entry) => {
    //交差の割合が1以上の場合(全体が見えている場合)
    if (entry.intersectionRatio >= 1) {
      entry.target.classList.add('allInView');
      span.textContent = '.allInView';
    } else { //交差の割合が1未満の場合(一部が見えていない場合)
      entry.target.classList.remove('allInView');
      span.textContent = '';
    }
  }); 
}

全体が見えなくなったら

前述の例では、全体が見えたらアニメーションを開始し、一部が見えなくなったら停止していますが、全体が見えなくなったら停止するようにするには threshold に [0, 1] を指定して、全体が見えなくなった際にもコールバック関数は呼び出すようにします。

threshold に 0 を指定すると、対象の要素が見え始める時と見えなくなる時にコールバック関数が呼び出されます。threshold に複数の値を指定する場合は、配列で指定します。

以下は対象の全体が見えたらアニメーションを開始し、全体が見えなくなったらアニメーションを停止する例です(HTML と CSS は前述の例と同じ)。

//オプション
const options = { 
  //対象の要素が領域に出入りする際(0)との全体が表示される際(1)を指定
  threshold: [0, 1],
}

const span = document.getElementById('span');

//コールバック関数の定義
const callback = (entries) => {
  //各 entry に対して
  entries.forEach( (entry) => {
    //交差の割合が1以上の場合(全体が見えている場合)
    if (entry.intersectionRatio >= 1) {
      entry.target.classList.add('allInView');
      span.textContent = '.allInView';
    //交差の割合が0の(見えなくなった)場合  
    } else if(entry.intersectionRatio === 0){ 
      entry.target.classList.remove('allInView');
      span.textContent = '';
      //console.log('removed'); //確認用
    }
  }); 
}
  
const observer = new IntersectionObserver(callback, options);  
const targets = document.querySelectorAll('.target');
targets.forEach( (elem) => {
  observer.observe(elem);
});

一定の割合になったら実行

thresholdintersectionRatio を使って対象の要素が表示されて一定の割合に達したら何らかの処理、例えばアニメーションを開始したり、停止することができます。

以下は対象の要素の75%以上が見えたらアニメーションを開始し、30%以下になったらアニメーションを停止する例です。

console.log() の行のコメントを外すと実際にコールバック関数が呼び出された際の intersectionRatio の値を確認することができますが、必ずしも threshold で指定した割合と完全に一致するわけではありません。

そのため、intersectionRatio の値の判定では、割合が一致した、等しい場合(=== や ==)で判定するのではなく、その値以上や以下、または未満などの比較演算子( >= や < など)を使って判定します。

//オプション
const options = { 
  //交差の割合が 0.3 または 0.75 でコールバック関数を呼び出す
  threshold: [.3, .75],
}

//テキストを出力する要素を取得
const span = document.getElementById('span');

//コールバック関数の定義
const callback = (entries) => {
  //各 entry に対して
  entries.forEach( (entry) => {
    //交差の割合が0.75以上の場合
    if (entry.intersectionRatio >= .75) {
      entry.target.classList.add('partInView');
      span.textContent = '.partInView';
      //console.log('added'); //確認用
      //console.log(entry.intersectionRatio); //確認用
    //交差の割合が 0.3 以下の場合  
    } else if(entry.intersectionRatio <= .3){ 
      entry.target.classList.remove('partInView');
      span.textContent = '';
      //console.log('removed'); //確認用
      //console.log(entry.intersectionRatio); //確認用
    }
  }); 
}
  
const observer = new IntersectionObserver(callback, options);  
const targets = document.querySelectorAll('.target');
targets.forEach( (elem) => {
  observer.observe(elem);
});

.target

HTML と CSS は前述の例とほぼ同じです。

動作を確認するには前後にある程度の高さがある要素が必要です
<div class="target">
  <p>.target <span id="span"></span></p>
</div>
.target {
  width: 300px;
  height: 200px;
  background-color: rebeccapurple;
  color: #fff;
  text-align: center;
  line-height: 200px;
}

.partInView {
  animation: backgroundChange 9s infinite;
}

@keyframes backgroundChange {
  0% {
    background-color: rebeccapurple;
  }
  20% {
    background-color:crimson;
  }
  45% {
    background-color:darkcyan;
  }
  70% {
    background-color:darkkhaki;
  }
  90% {
    background-color:darksalmon;
  }
}
  

グラデーションのアニメーション

以下は対象の要素の交差している割合(intersectionRatio)を使ってグラデーションのアニメーションを表示する例です。

IntersectionObserver Gradient

<div class="gradientTarget"> 
  <p>IntersectionObserver Gradient</p>
</div>

この例では監視対象とグラデーションを適用する要素を同じ要素にしています。

JavaScript がオフの場合や IntersectionObserver が使えない場合を考慮して CSS で線形グラデーションを設定しています。

線形グラデーションの始点や中間点、終点の位置はパーセントで指定しています。100% を超えたり、負の値も指定できます。

/*監視対象及びグラデーションを適用する要素*/
.gradientTarget {
  /*線形グラデーション*/
  background-image: linear-gradient(
    royalblue -80%, /* 始点 */
    pink 40%,
    palegoldenrod 80%,
    mediumaquamarine 180%,
    blue 300% /* 終点 */
  );
  padding: 1rem 2rem;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  min-height: 300px;
  color: #fff;
}
   
.gradientTarget p {
  line-height: 300px;
  width: 100%;
  margin: 0 auto;
  font-size: 1.75rem;
  font-weight: bold;
  text-shadow: 0.5px 0.5px 0.5px #666;
}

1〜11行目は threshold に指定する値を作成して返す関数です。なめらかな表示にするため、0.01刻みの値を100 +1 個指定して、交差する率が 1% 変化するごとにコールバック関数を呼び出すようにしています(実際には30ぐらい指定すれば十分かと思いますが、あまり少ないとカクカクします)。

この例の場合も、オブザーバーの処理を関数(ioGradient)として定義しています。関数とせずに、17〜55行目までをそのまま記述することもできます。

グラデーションの設定では監視対象の要素が領域と交差している割合(intersectionRatio)を使って、グラデーションのカラーストップの位置を算出しています。intersectionRatio は 0.0 〜 1.0 の間の数値なのでパーセントに変換しています(30行目)。

この例では intersectionRatio を100倍(* 100)して四捨五入していますが、100 の代わりに * 50 としたり、* 200 とすればグラデーションの変化の割合を調整することもできます。

これにより、交差の割合が変化する間グラデーションのアニメーションが表示されます(対象の要素全体が表示されている際は、交差している割合は1のままになるのでアニメーションにはなりません)。

グラデーションの角度もアニメーションするには、例えば34行目を ${90 * entry.intersectionRatio }deg などとできます。

//閾値(threshold)を作成して返す関数
const makeThresholdArray = (count, one=false) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      threshold.push(i/count);
    }
  }
  if(one) threshold.push(1);
  return threshold;
}  
  
//グラデーションアニメーションを設定する関数  
const ioGradient = () => {
  
  //オプション
  const options = {
    //20% 見えてからコールバック関数を呼び出す
    rootMargin: '-20% 0px',
    //0.01刻みの100個(+1)の閾値を指定(実際は30個ぐらいで十分)
    threshold: makeThresholdArray(100, true)
  };

  //コールバック関数
  const callback = (entries) => {
    entries.forEach((entry) => {
      //交差している場合
      if (entry.isIntersecting) {
        //交差の割合を%に変換(四捨五入)
        const ratio = Math.round(entry.intersectionRatio * 100);
        //交差の割合を使ってグラデーションを設定
        entry.target.style.backgroundImage = `
          linear-gradient(
            180deg,
            royalblue ${-80 - ratio}%,
            pink ${40 - ratio}%,
            palegoldenrod ${80 - ratio}%,
            mediumaquamarine ${180 - ratio}%,
            blue ${300 - ratio}%
        )`;
      }
    });
  }
  
  //引数にコールバック関数とオプションを指定してオブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
  
  //監視対象の要素(gradientTarget クラスを指定した要素)を全て取得
  const gradientTargets = document.querySelectorAll('.gradientTarget');
  
  //全ての監視対象要素を observe() メソッドに指定
  gradientTargets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
}
//上記関数を実行
ioGradient(); 

HSL でグラデーション

以下は HSL カラーを使ったグラデーションのアニメーションの例です。前述の例はグラデーションの位置を割合(intersectionRatio)を使って変更していますが、この例では 色相(Hue)の変化に intersectionRatio の値を利用しています。

彩度(Saturation)や輝度(Lightness)の値を変化させることもできます。

※ IntersectionObserver の使い方に関しては前述の例と同じです。

IntersectionObserver Gradient

<div class="hslGradientTarget"> 
  <p>IntersectionObserver Gradient</p>
</div>

内容的には前述の例と同じですが、この例では対象の要素とグラデーションの色を引数に指定できるようにしています。

関数 ioHslGradient() には、少なくとも対象の要素のクラス名を第1引数に指定する必要があります。

第2引数の hslOption にはグラデーションのカラーストップの数や開始の Hue の値などを指定できます(省略した場合はデフォルトが適用されます)。

//閾値(threshold)を作成して返す関数
const makeThresholdArray = (count, one=false) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      threshold.push(i/count);
    }
  }
  if(one) threshold.push(1);
  return threshold;
}

/*
HSL でグラデーションを生成する関数(以下の引数を受け取ります)
targetClassName:グラデーションを適用する要素のクラス名(必須)
hslOption :HSLカラーのオプション
ratioRate:交差の割合を調整する値(大きな値ほど変化が大きくなる)
*/  
  
//グラデーションアニメーションを設定する関数  
const ioHslGradient = (targetClassName, hslOption, ratioRate) => {
 
  //オプション(環境に合わせて)
  const options = {
    //20% 見えてからコールバック関数を呼び出す
    rootMargin: '-20% 0px',
    //0.01刻みの100個(+1)の閾値を指定(実際は30個ぐらいで十分)
    threshold: makeThresholdArray(50, true)
  };
  
  //グラデーションの色(引数に値が渡されていればその値を使い、そうでなければデフォルトを設定)
  if(hslOption) {
    hslOption = hslOption;
  }else{
    //デフォルトのグラデーション
    hslOption = {
      length: 4, //カラーストップの数
      angle : '45deg', //グラデーションの角度
      hueStart: 160, //最初の色の色相の値
      hueStep: 30, //各色の色相の間隔 
      saturation: '70%', //彩度
      lightness: '80%'  //輝度
    };
  }
  
  //交差の割合を調整する値(大きな値ほど変化が大きくなる)
  if(ratioRate) {
    ratioRate = parseInt(ratioRate);
  }else{
    ratioRate = 100; //デフォルト
  }
 
  //コールバック関数
  const callback = (entries) => {
    entries.forEach((entry) => {
      //交差している場合
      if (entry.isIntersecting) {
        //交差の割合を Hue に適用する値に変換(ratioRate で割合を調整)
        const ratio = Math.round(entry.intersectionRatio * ratioRate);
        //ratio と引数の値を使ってグラデーションを設定
        let gradient = ' linear-gradient( ' + hslOption.angle + ',';
        for(let i=0; i<hslOption.length; i++) {
          gradient += 'hsl(' + (parseInt(hslOption.hueStart) + parseInt(hslOption.hueStep) * i  + ratio) + ',' + 
            hslOption.saturation + ',' +
            //彩度も変化させる場合は例えば上記を以下のようにするなどできます
            //parseInt(hslOption.saturation) * entry.intersectionRatio  + '%,' +
            hslOption.lightness + ') ' + 
            (100/hslOption.length) * i + '%';
          if(i !== hslOption.length -1) gradient += ',';
        }
        gradient += ')';
        entry.target.style.backgroundImage = gradient;
        //gradient の値をコンソールに出力(確認用)
        //console.log(gradient);
      }
    });
  }
  
  //引数にコールバック関数とオプションを指定してオブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
  
  //監視対象の要素(引数で指定された要素)を取得
  const targets = document.querySelectorAll('.' + targetClassName);
  //全ての監視対象要素を observe() メソッドに指定
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
  
}
//色のオプション 
const hslColor = {
  length: 4, 
  angle : '45deg', 
  hueStart: 160, 
  hueStep: 30, 
  saturation: '70%', 
  lightness: '80%' 
};
//上記色のオプションと対象の要素、交差の割合を調整する値を指定して関数を実行
ioHslGradient('hslGradientTarget', hslColor, 70);  

CSS に指定する background-image の値は、上記 JavaScript の72行目のコメントを外してグラデーションの値をコンソールに出力してコピーすることができます。

.hslGradientTarget {
  background-image:linear-gradient( 
    45deg,
    hsl(220, 70%, 80%) 0%,
    hsl(250, 70%, 80%) 25%,
    hsl(280, 70%, 80%) 50%,
    hsl(310, 70%, 80%) 75%
  );
  padding: 1rem 2rem;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  min-height: 300px;
}

.hslGradientTarget p {
  line-height: 300px;
  width: 100%;
  margin: 0 auto;
  font-size: 1.75rem;
  font-weight: bold;
  color: #fff;
  text-shadow: 0.5px 0.5px 0.5px #666;
}

監視対象とアニメーションを適用する要素を異なる要素に

以下は監視対象の要素とグラデーションを適用する要素を異なる要素に設定する例です。

HTML では監視対象の要素からグラデーションを適用する要素を JavaScript で特定して取得できるように 全体を div 要素で囲んだ構成にします(この例では全体を囲む要素に gradientWrapper というクラスを指定していますが、クラスの指定は必須ではありません)。

<div class="gradientWrapper">
  <div class="gradientElem"><!--グラデーションを適用する要素 .gradientElem-->
    <p>IntersectionObserver Gradient</p>
  </div>
  <div class="targetElem"><!--監視対象の要素 .targetElem-->
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>

グラデーションを適用する要素は監視対象の要素(entry.target)の親要素(.parentElement)を起点に querySelector() で gradientElem クラスを指定した要素を取得します。

また、この例では閾値に指定する値は30個にしていますが、100個指定した場合とあまり変わりません。グラデーションの角度はこの例では 90deg にしています。

閾値を生成する関数(makeThresholdArray)は前述の例と同じです。

//閾値(threshold)を作成して返す関数
const makeThresholdArray = (count, one=false) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      threshold.push(i/count);
    }
  }
  if(one) threshold.push(1);
  return threshold;
} 

//グラデーションアニメーションを設定する関数  
const ioGradientWithTarget = () => {
  
  const options = {
    //0.0333刻みの30個(+1)の閾値を指定(別途定義した関数 makeThresholdArray を使用)
    threshold: makeThresholdArray(30, true)
  };
 
  const callback = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const ratio = Math.round(entry.intersectionRatio * 100);
        
        //グラデーションを適用する要素(監視対象の親要素から .gradientElem を取得)
        const gradientElem = entry.target.parentElement.querySelector('.gradientElem'); 
        
        //交差の割合を使ってグラデーションを(グラデーションを適用する要素に)設定
        gradientElem.style.backgroundImage = `
          linear-gradient(
            90deg,
            royalblue ${-80 - ratio}%,
            pink ${40 - ratio}%,
            palegoldenrod ${80 - ratio}%,
            mediumaquamarine ${180 - ratio}%,
            blue ${300 - ratio}%
        )`;
      }
    });
  }
  //オブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
  
  //監視対象の要素(targetElem クラスを指定した要素)を全て取得
  const targets = document.querySelectorAll('.targetElem');
  
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
}
//上記関数を実行
ioGradientWithTarget(); 
.gradientElem {
  background-image: linear-gradient( 
    90deg, 
    royalblue -80%, 
    pink 40%, 
    palegoldenrod 80%, 
    mediumaquamarine 180%, 
    blue 300% 
  );
  padding: 1rem 2rem;
  margin: 50px 0;
  width: 100%;
  max-width: 600px;
  min-height: 50px;
  color: #fff;
  text-shadow: 0.5px 0.5px 0.5px #666;
}
.gradientElem p {
  line-height: 50px;
  width: 100%;
  margin: 0 auto;
  font-size: 1.5rem;
  font-weight: bold;
}

IntersectionObserver Gradient

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum ex vel soluta alias, incidunt fugit, distinctio deserunt asperiores quidem quibusdam, nostrum necessitatibus ipsam esse, nam cum accusamus adipisci repellendus autem.

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

Quibusdam iure dolor dolores quas, repudiandae voluptas error obcaecati nemo praesentium, qui enim aliquid molestiae ipsum deserunt! Ipsum dolorem assumenda et iure vero exercitationem molestiae illo eius, voluptatum minima blanditiis?

関数の引数にオプションを渡す

今までのサンプルでは、オブザーバーの生成や監視対象の要素の指定などをまとめて関数に定義していますが、必要に応じてその関数の引数にオプションを指定することもできます。

以下は引数に対象の要素やグラデーションの値を渡すようにした例です。値を指定しなければ、デフォルトのグラデーションで表示します(あまり実用的ではありませんが)。

HTML の構造や CSS は前述のサンプルと同じですが、この場合、監視対象の要素とグラデーションを適用する要素には JavaScript の関数の引数に指定するクラス名を設定する必要があります。

基本的な HTML の構造
<div class="gradientWrapper"><!--全体を div 要素で囲む -->
  <div class="gradientElem"><!--グラデーションを適用する要素(クラス名を引数に指定)-->
    <p>IntersectionObserver Gradient</p>
  </div>
  <div class="targetElem"><!--監視対象の要素(クラス名を引数に指定)-->
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>

閾値を作成して返す関数(makeThresholdArray)をは前述の例と同じです。

関数 ioGradientAnimation() の引数には、グラデーションを適用する要素と監視対象の要素のクラス名を指定する必要があります(必須)。その他は省略すれば、デフォルトの値が適用されます。

また、グラデーションを適用する要素と監視対象の要素を同じ要素に指定することもできます。

//閾値(threshold)を作成して返す関数(前述の例と同じ)
const makeThresholdArray = (count, one=false) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      threshold.push(i/count);
    }
  }
  if(one) threshold.push(1);
  return threshold;
} 
 
/*  
グラデーションアニメーションを設定する関数 ioGradientAnimation 以下の引数を指定 
gradientTarget:グラデーションを適用する要素のクラス名(必須)
observerTarget:監視対象の要素のクラス名(必須)
gradientAngle:グラデーションの角度(オプション)
gradientColors:グラデーションの色と位置のオブジェクトの配列(オプション)
rootMargin:オブザーバー生成時の rootMargin (オプション。デフォルトは '0px')
threshold:オブザーバー生成時の threshold (オプション。デフォルトは 30個の閾値)
*/
             
const ioGradientAnimation = (gradientTarget, observerTarget, gradientAngle, gradientColors, rootMargin, threshold) => {
  //グラデーションの色(引数に値が渡されていればその値を使い、そうでなければデフォルトを設定)
  if(gradientColors) {
    gradientColors = gradientColors;
  }else{
    //デフォルトのグラデーション
    gradientColors = [
      { color: 'royalblue', position: -80 }, 
      { color: 'pink', position: 40 }, 
      { color: 'palegoldenrod', position: 80 }, 
      { color: 'mediumaquamarine', position: 180 },
      { color: 'blue', position: 300 }  
    ];
  }
  //グラデーションの角度
  if(gradientAngle) {
    gradientAngle = gradientAngle;
  }else{
    gradientAngle = '45deg';//デフォルト
  }
  
  //引数にオプションが指定されていればそれらの値を設定
  const options = {
    rootMargin: rootMargin ? rootMargin : '0px',
    threshold: threshold ? threshold : makeThresholdArray(30, true)
  };
  
  const callback = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        //交差の割合(intersectionRatio)を%に変換
        const ratio = Math.round(entry.intersectionRatio * 100);
        
        //グラデーションを適用する要素   
        let gradientElem;
        if(gradientTarget === observerTarget) {
          //グラデーションを適用する要素と監視する要素が同じ場合
          gradientElem = entry.target;
        }else{
          //グラデーションを適用する要素(引数で指定された要素)を取得
          gradientElem = entry.target.parentElement.querySelector('.' + gradientTarget); 
        }
        
        //引数の値(またはデフォルト)と交差の割合を使ってグラデーションの値を作成
        let gradient = ' linear-gradient( ' + gradientAngle + ',';
        for(let i=0; i<gradientColors.length; i++) {
          gradient += `${gradientColors[i].color} ${gradientColors[i].position - ratio}%`;
          if(i !== gradientColors.length -1) gradient += ',';
        }
        gradient += ')';
        gradientElem.style.backgroundImage = gradient;
      }
    });
  }
  //オブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
  //監視対象の要素(引数で指定された要素)を全て取得
  const targets = document.querySelectorAll('.' + observerTarget);
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
}
 
//監視対象の要素とグラデーションを適用する要素を指定してデフォルトのグラデーションで表示
ioGradientAnimation('gradient1', 'target1'); 

//引数に指定するグラデーションの色と位置を指定したオブジェクトの配列
const gc = [
  { color: '#B9ECBA', position: -60 }, 
  { color: '#ECF28E', position: 30 }, 
  { color: '#F6C07E', position: 120 }, 
  { color: '#F44B4E', position: 240 },
];
 
ioGradientAnimation(
  'gradient2', 
  'gradient2', 
  '180deg',
  gc, //上記グラデーション
  '-25% 0px', 
  makeThresholdArray(60, true)
);  
<div class="gradientWrapper">
  <div class="gradient1">
    <p>gradient 1 </p>
  </div>
  <div class="target1">
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>
  
<div class="gradientWrapper">
  <div class="gradient2">
    <p>gradient 2</p>
  </div>
</div>
  
<div class="gradientWrapper">
  <div class="gradient1">
    <p>gradient 1 </p>
  </div>
  <div class="target1">
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>
.gradient1 {
  background-image: linear-gradient( 
    90deg, 
    royalblue -80%, 
    pink 40%, 
    palegoldenrod 80%, 
    mediumaquamarine 180%, 
    blue 300% 
  );
  padding: 1rem;
  margin: 30px 0;
  width: 100%;
  max-width: 600px;
  min-height: 50px;
  color: #fff;
  text-shadow: 0.5px 0.5px 0.5px #666;
}
.gradient1 p  {
  line-height: 50px;
  width: 100%;
  margin: 0 auto;
  font-size: 1.75rem;
  font-weight: bold;
}
  
.gradient2 {
  background-image: linear-gradient( 
    90deg, 
    #333 -80%, 
    #666 40%, 
    #999 80%, 
    #ccc 180%, 
    yellow 300% 
  );
  padding: 1rem;
  margin: 30px 0;
  width: 100%;
  max-width: 600px;
  min-height:200px;
  color: #fff;
  text-shadow: 0.5px 0.5px 0.5px #333;
}
.gradient2 p  {
  line-height: 200px;
  width: 100%;
  margin: 0 auto;
  font-size: 1.75rem;
  font-weight: bold;
}

gradient 1

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum ex vel soluta alias, incidunt fugit, distinctio deserunt asperiores quidem quibusdam, nostrum necessitatibus ipsam esse, nam cum accusamus adipisci repellendus autem.

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

Quibusdam iure dolor dolores quas, repudiandae voluptas error obcaecati nemo praesentium, qui enim aliquid molestiae ipsum deserunt! Ipsum dolorem assumenda et iure vero exercitationem molestiae illo eius, voluptatum minima blanditiis?

gradient 2

gradient 1

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum ex vel soluta alias, incidunt fugit, distinctio deserunt asperiores quidem quibusdam, nostrum necessitatibus ipsam esse, nam cum accusamus adipisci repellendus autem.

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

円形グラデーションサンプル

<style>
.rgTarget1 {
  background-image: radial-gradient( circle farthest-corner at center, hsl(120, 80%, 70%) 0%, hsl(140, 80%, 70%) 20%, hsl(160, 80%, 70%) 40%, hsl(180, 80%, 70%) 60%, hsl(200, 80%, 70%) 80%, hsl(220, 80%, 70%) 100% );
  padding: 1rem 2rem;
  margin: 50px 0;
  width: 100%;
  max-width: 400px;
  min-height: 200px;
}
.rgTarget2 {
  background-image: radial-gradient( circle farthest-side at bottom, hsla(60, 80%, 50%, 0) 72%, hsl(110, 80%, 50%) 76%, hsl(160, 80%, 50%) 80%, hsl(210, 80%, 50%) 84%, hsl(260, 80%, 50%) 88%, hsl(310, 80%, 50%) 92%, hsla(360, 80%, 50%, 0) 100% );
  padding: 1rem 2rem;
  margin: 50px;
  width: 100%;
  max-width: 300px;
  min-height: 150px;
}
.rgTarget1 p {
  line-height: 200px;
  width: 100%;
  margin: 0 auto;
  text-align: center;
  color: #4F72B5;
}
</style>

<div style="height:1000px;"></div>
<div class="gradientWrapper"><!--全体を div 要素で囲む -->
  <div class="rgTarget1">
    <p>監視対象及びグラデーションの対象</p>
  </div>
</div>
<div class="gradientWrapper"><!--全体を div 要素で囲む -->
  <div class="rgTarget2"></div>
  <div class="obTarget" style="height:600px; background-color: #eee;">監視対象要素</div>
</div>
<div style="height:1000px;"></div>
  
<script>
  
//閾値(threshold)を作成して返す関数
const makeThresholdArray = (count, one=false) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      threshold.push(i/count);
    }
  }
  if(one) threshold.push(1);
  return threshold;
}  
/*
HSL で円形グラデーションを生成する関数(以下の引数を受け取ります)
gradientTarget:グラデーションを適用する要素のクラス名(必須)
gradientTarget:監視対象の要素のクラス名(必須)
rgOptions :グラデーションのオプション
rootMargin:オブザーバー生成時の rootMargin (オプション。デフォルトは '0px')
threshold:オブザーバー生成時の threshold (オプション。デフォルトは 30個の閾値)
*/ 
//グラデーションアニメーションを設定する関数  
const ioHslRadialGradient = (gradientTarget, observerTarget, rgOptions, rootMargin, threshold) => {

  //グラデーションの色(引数に値が渡されていればその値を使い、そうでなければデフォルトを設定)
  if(rgOptions) {
    rgOptions = rgOptions;
  }else{
    //デフォルトのグラデーション
    rgOptions = {
      length: 5,   //色の数
      shape: 'circle', 
      size: 'farthest-corner', 
      position: 'at center', 
      hueStart: 0,  //Hue の最初の値
      hueStep: 10,  //Hue の変化する値
      saturation: '100%', 
      lightness: '50%',
      ratioRate : 100,
      positionStep : false, //位置を色の数で等分した値ではなく、数値で指定する場合にその度合を指定
      initPosition : 0, //positionStep を指定した場合の最初の位置
      opacity : 1 //positionStep を指定した場合の最初と最後の色の不透明
    } 
  }
  
  //引数にオプションが指定されていればそれらの値を設定
  const options = {
    rootMargin: rootMargin ? rootMargin : '0px',
    threshold: threshold ? threshold : makeThresholdArray(50, true)
  };
  
  //コールバック関数
  const callback = (entries) => {
    entries.forEach((entry) => {
      //交差している場合
      if (entry.isIntersecting) {
        
        //グラデーションを適用する要素   
        let gradientElem;
        if(gradientTarget === observerTarget) {
          //グラデーションを適用する要素と監視する要素が同じ場合
          gradientElem = entry.target;
        }else{
          //グラデーションを適用する要素(引数で指定された要素)を取得
          gradientElem = entry.target.parentElement.querySelector('.' + gradientTarget); 
        }
        
        //引数のオプションと交差の割合を使ってグラデーションを設定
        const length = parseInt(rgOptions.length);
        const shape = rgOptions.shape;
        const size = rgOptions.size;
        const position = rgOptions.position;
        const hueStart = parseInt(rgOptions.hueStart);
        const hueStep = parseInt(rgOptions.hueStep);
        const saturation = rgOptions.saturation;
        const lightness = rgOptions.lightness;
        const ratioRate = parseInt(rgOptions.ratioRate);
        const positionStep = rgOptions.positionStep ? parseInt(rgOptions.positionStep): false;
        const initPosition = parseInt(rgOptions.initPosition);
        const opacity = parseFloat(rgOptions.opacity);
        //交差の割合を Hue に適用する値に変換
        const ratio = Math.round(entry.intersectionRatio * rgOptions.ratioRate);
        let gradient = ' radial-gradient( ' + shape + ' ' + size + ' ' + position + ',';
        //length の値や引数の positionStep に値が指定されている場合により分岐
        for(let i=0; i<length; i++) {
          if(positionStep) {
            if(i === 0){
              gradient += `hsla(${(hueStart + hueStep * i )+ ratio}, ${saturation}, ${lightness}, ${opacity}) ${initPosition}%`;
            }else if(i === length -1){
              gradient += `hsla(${(hueStart + hueStep * i )+ ratio}, ${saturation}, ${lightness}, ${opacity}) ${100}%`;
            }else{
              gradient += `hsl(${(hueStart + hueStep * i )+ ratio}, ${saturation}, ${lightness}) ${initPosition + positionStep * i}%`;
            }
          }else{
            gradient += `hsl(${(hueStart + hueStep * i )+ ratio}, ${saturation}, ${lightness}) ${(100/length) * i}%`;
            if(i === length -1) {
              gradient += `, hsl(${(hueStart + hueStep * (i +1) )+ ratio}, ${saturation}, ${lightness}) ${100}%`;
            }
          }
          if(i !== length -1) gradient += ',';
        }
        gradient += ')';
        gradientElem.style.backgroundImage = gradient;
        console.log(gradient)
      }
    });
  }
  const observer = new IntersectionObserver(callback, options); 
  //監視対象の要素(引数で指定された要素)を全て取得
  const targets = document.querySelectorAll('.' + observerTarget);
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
}

//グラデーションのオプション(第3引数)
const radialGradient = {
  length: 3,   //色の数
  shape: 'circle', 
  size: 'farthest-corner', 
  position: 'at center', 
  hueStart: 60, 
  hueStep: 30, 
  saturation: '80%', 
  lightness: '70%',
  ratioRate : 60,
};
//上記オプションを指定して関数を実行
ioHslRadialGradient('rgTarget1', 'rgTarget1',radialGradient, '-10% 0px');
  
const rainbow = {
  length: 9,   //色の数
  shape: 'circle', 
  size: 'farthest-side', 
  position: 'at bottom', 
  hueStart: 160, 
  hueStep: 40, 
  saturation: '90%', 
  lightness: '65%',
  ratioRate : 160,
  positionStep :4, //位置を数値で指定する場合。初期値は
  initPosition : 70, //positionStep を指定した場合の最初の位置
  // length * positionStep + initPosition の最大値は 100
  opacity : 0 //positionStep を指定した場合の最初と最後の色の不透明度(非表示にするには0を指定)
  
}  
//上記オプションを指定して関数を実行(サンプルに表示している虹のようなグラデーション)
ioHslRadialGradient('rgTarget2', 'obTarget',rainbow, '10% 0px');   
</script>
クリッピング

グラデーションを適用した要素に CSS の background-clip: text や clip-path を設定して切り抜き(クリッピング)すると面白い効果になります。以下のサイトを参考にさせていただきました。

JavaScriptのIntersection Observerでスクロールに合わせてグラデーションの色を変更する

前述のいずれかの方法でグラデーションを適用した要素に CSS で background-clip: text や clip-path を設定してテキストや図形をクリッピングします。

以下の例ではグラデーションを適用した要素に、clipText や clipTriangle、clipCircle などのクラスを指定してクリッピングを設定しています。

グラデーションのアニメーションは前述の「関数に引数のオプションを渡す」で定義した関数を使って、.gradient1 と .gradient3 の要素に異なるアニメーションを設定しています。

//前述の例と同じ関数(閾値を生成する関数は省略)          
const ioGradientAnimation = (gradientTarget, observerTarget, gradientAngle, gradientColors, rootMargin, threshold) => {
  //グラデーションの色(引数に値が渡されていればその値を使い、そうでなければデフォルトを設定)
  if(gradientColors) {
    gradientColors = gradientColors;
  }else{
    //デフォルトのグラデーション
    gradientColors = [
      { color: 'royalblue', position: -80 }, 
      { color: 'pink', position: 40 }, 
      { color: 'palegoldenrod', position: 80 }, 
      { color: 'mediumaquamarine', position: 180 },
      { color: 'blue', position: 300 }  
    ];
  }
  //グラデーションの角度
  if(gradientAngle) {
    gradientAngle = gradientAngle;
  }else{
    gradientAngle = '45deg';//デフォルト
  }
  
  //引数にオプションが指定されていればそれらの値を設定
  const options = {
    rootMargin: rootMargin ? rootMargin : '0px',
    threshold: threshold ? threshold : makeThresholdArray(30, true)
  };
  
  const callback = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        //交差の割合(intersectionRatio)を%に変換
        const ratio = Math.round(entry.intersectionRatio * 100);
        
        //グラデーションを適用する要素   
        let gradientElem;
        if(gradientTarget === observerTarget) {
          //グラデーションを適用する要素と監視する要素が同じ場合
          gradientElem = entry.target;
        }else{
          //グラデーションを適用する要素(引数で指定された要素)を取得
          gradientElem = entry.target.parentElement.querySelector('.' + gradientTarget); 
        }
        
        //引数の値(またはデフォルト)と交差の割合を使ってグラデーションの値を作成
        let gradient = ' linear-gradient( ' + gradientAngle + ',';
        for(let i=0; i<gradientColors.length; i++) {
          gradient += `${gradientColors[i].color} ${gradientColors[i].position - ratio}%`;
          if(i !== gradientColors.length -1) gradient += ',';
        }
        gradient += ')';
        gradientElem.style.backgroundImage = gradient;
      }
    });
  }
  //オブザーバーを生成
  const observer = new IntersectionObserver(callback, options); 
  //監視対象の要素(引数で指定された要素)を全て取得
  const targets = document.querySelectorAll('.' + observerTarget);
  targets.forEach( (elem) => {
    //observe() に監視対象の要素を指定
    observer.observe(elem);
  });
}
 
//監視対象の要素とグラデーションを適用する要素を指定してデフォルトのグラデーションで表示
ioGradientAnimation('gradient1', 'target1'); 

//.gradient3 に適用するグラデーションの色と位置
const gc2 = [
  { color: 'cornflowerblue', position: -80 }, 
  { color: 'pink', position: 40 }, 
  { color: 'orange', position: 80 }, 
  { color: 'lightgreen', position: 180 },
  { color: 'royalblue', position: 260 },
];
 
//.gradient3 は自身を監視対象として自身にグラデーションアニメーションを適用
ioGradientAnimation(
  'gradient3', //グラデーションを適用する要素
  'gradient3', //監視対象の要素(上記と同じ要素)
  '45deg',
  gc2, //上記グラデーション
  '-25% 0px',  //rootMargni のオプション
  makeThresholdArray(60, true) //threshold のオプション
); 
<div class="gradientWrapper bg-black">
  <div class="gradient1 clipText"><!--テキストをクリッピング-->
    <p>IntersectionObserver </p>
  </div>
  <div class="target1">
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>
<div class="gradientWrapper bg-black">
  <div class="gradient3 clipTriangle"><!--三角形をクリッピング--></div>
</div>
<div class="gradientWrapperk">
  <div class="gradient1 clipText"><!--テキストをクリッピング-->
    <p>IntersectionObserver </p>
  </div>
  <div class="target1">
    <p>Lorem ipsum dolor sit... vitae hic recusandae.</p>
    <p>Iste, nemo, ... Inventore quos harum beatae.</p>
    <p>Sint magnam pariatur... possimus voluptate.</p>
  </div>
</div>
<div class="gradientWrapper">
  <div class="gradient3 clipCircle"> </div><!--円形をクリッピング-->
</div>

テキストを切り抜くグラデーションを適用した要素には clipText クラスを指定して、text-fill-color: transparentbackground-clip: text を指定しています。

三角形を切り抜くには clip-path: polygon() を使っています。円は clip-path: circle(50%) で切り抜くことができますが、正方形に border-radius: 50% を指定してもその背景がグラデーションになるので同じ結果が得られます。

bg-black クラスを指定してある要素は背景色に黒を指定しています。

※ background-clip: text を指定した要素に text-shadow を指定するとうまくクリップできません。

以下ではそれぞれの要素の領域がわかりやすいように点線の枠線を表示しています。わかりにくいですが、三角形と円にも表示されています。

IntersectionObserver

上の「IntersectionObserver」部分のグラデーションはこの点線部分の交差状態を監視しています。

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum ex vel soluta alias, incidunt fugit, distinctio deserunt asperiores quidem quibusdam, nostrum necessitatibus ipsam esse, nam cum accusamus adipisci repellendus autem.

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

下の三角の図形のグラデーションは自身の要素を交差の監視対象としています。但し IntersectionObserver を生成する際の rootMargin に '-25% 0px' を指定しているので、図形が見えてから 25% が過ぎた時点からアニメーションが開始されます。

IntersectionObserver

上の「IntersectionObserver」部分のグラデーションはこの点線部分の交差状態を監視しています。

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum ex vel soluta alias, incidunt fugit, distinctio deserunt asperiores quidem quibusdam, nostrum necessitatibus ipsam esse, nam cum accusamus adipisci repellendus autem.

Amet totam alias blanditiis eligendi vel a laborum veritatis voluptates dolorum commodi aliquid, impedit cupiditate! Quaerat consectetur aliquid, ratione aut enim unde quisquam magnam id cumque temporibus aspernatur totam et.

下の円の図形のグラデーションは自身の要素を交差の監視対象としています。但し IntersectionObserver を生成する際の rootMargin に '-25% 0px' を指定しているので、図形が見えてから 25% が過ぎた時点からアニメーションが開始されます。

スクロールスパイ

スクロールスパイは以下のような要素が表示されている位置に対応するリンクをハイライトするなどして示す機能です。以下のボックス内でスクロールすると、表示されている要素の位置に対応するメニューをハイライト表示します。

以下の例の場合、監視する領域(枠線で囲まれたボックス)の上から25%を境界として交差を判定しています。この例のような場合は、監視する領域を矩形ではなく、境界線として交差を判定したほうが扱いやすくなります(詳細:境界で判定)。

.target #blue
.target #green
.target #red
<div class="wrapper">
  <div id="sticky">
    <ol class="nav">
      <li><a href="#blue">blue</a></li>
      <li><a href="#green">green</a></li>
      <li><a href="#red">red</a></li>
    </ol>
  </div>
  <div id="container">
    <div id="blue" class="target">.target #blue</div>
    <div id="green" class="target">.target #green</div>
    <div id="red" class="target">.target #red</div>
  </div>
</div>
//オプション
const options = {
  root: document.querySelector('.wrapper'),
  //上から25%の位置を交差の境界とする
  rootMargin: '-25% 0px -75%', 
  threshold: 0 //デフォルトなので省略可能
}
//コールバック関数
const callback = (entries) => {
  // 交差を検知をしたら
  entries.forEach(entry => {
    //監視対象の要素が領域(境界)と交差していれば(isIntersecting が true の場合)
    if (entry.isIntersecting) {
      //その要素を引数として以下の関数を呼び出してメニューをハイライト
      highlightNavMenu(entry.target);
    }
  });
}
//オブザーバーを生成
const observer = new IntersectionObserver(callback, options);
//監視対象の要素を取得
const targets = document.querySelectorAll('.target');
//それぞれの監視対象の要素を observe() に指定して監視
targets.forEach( (target) => {
  observer.observe(target);
});

//メニュー項目をハイライトする(色を変える)関数 
const highlightNavMenu = (target) => {
  //現在アクティブになっている(active クラスが付与されている)メニューの要素を取得
  const highlightedMenu = document.querySelector('.nav .active');

  //上記で取得した要素が存在すれば、その要素から active クラスを削除
  if (highlightedMenu !== null) {
    highlightedMenu.classList.remove('active');
  }

  //引数で渡された現在交差している(isIntersecting が true の)要素の id 属性からリンク先(href)の値を生成
  const href = '#' + target.id;

  //上記で生成したリンク先を持つメニューが、現在交差している要素のリンク
  const currentActiveMenu =  document.querySelector(`a[href='${href}']`);

  //現在交差している要素のリンクに active クラスを追加
  currentActiveMenu.classList.add('active');
}
#sticky {
  position: sticky;
  top: 0;
  height: 40px;
  background-color: #eee;
}
#container {
  margin: 100px 0 350px;
}
.nav {
  width: 100%;
  display: flex;
  justify-content: space-around;
  margin: 0;
}
.nav {
  padding-left: 0;
}
.nav li {
  list-style: none;
  color: #70B466;
}
.nav a {
  display: block;
  width: 100%;
  padding: .25rem .5rem;
  text-decoration: none;
  color: #70B466;
}
.nav a.active {
  color: #D0F0C1;
  background-color: #557E49;
}
.wrapper {
  height: 400px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}
.target {
  width: 200px;
  margin: 50px auto;
  text-align: center;
  color: #fff;
}
#blue {
  background-color: cornflowerblue;
  height: 120px;
  line-height: 120px;
}
#green {
  background-color: darkseagreen;
  height: 200px;
  line-height: 200px;
}
#red {
  background-color: palevioletred;
  height: 160px;
  line-height: 160px;
}

実際に使用する場合は、上記のような特定の領域を監視対象とするのではなく、以下のサンプルのようにブラウザのビューポートを監視対象とすることが多いと思います。

サンプルを開く

以下がサンプルの HTML です。

メニュー項目のリンク先(href の値)は、それぞれのセクション(div 要素)の id になっていて、監視対象の要素は linkTarget クラスの div 要素になります。

※ このサンプルのスクロールスパイの場合、最後のリンク先の下に十分なスペースがない場合、最後のリンクがハイライトされない可能性があるので、この例ではフッターにある程度の高さを指定しています。

<div class="wrapper">
<h1> IntersectionObserver Scroll Spy Sample </h1>
  <nav class="nav-wrapper">
    <ol id="navigation" class="nav">
      <li><a href="#section1">Section 1</a></li>
      <li><a href="#section2">Section 2 </a></li>
      <li><a href="#section3">Section 3</a></li>
      <li><a href="#section4">Section 4</a></li>
      <li><a href="#section5">Section 5</a></li>
    </ol>
  </nav>
  <main class="contents">
    <div class="linkTarget" id="section1">
      <h2>Section 1</h2>
      <p>Lorem ipsum dolor sit amet, ... ipsa ad tenetur in.</p>
      ・・・中略・・・
    </div>
    <div class="linkTarget" id="section2">
      <h2>Section 2</h2>
      <p>Lorem ipsum dolor sit amet, ... sint enim.</p>
       ・・・中略・・・
    </div>
    <div class="linkTarget" id="section3">
      <h2>Section 3</h2>
      <p>Lorem ipsum dolor sit amet, ...veniam velit deleniti.</p>
       ・・・中略・・・
    </div>
    <div class="linkTarget" id="section4">
      <h2>Section 4</h2>
      <p>Lorem ipsum dolor sit amet,...rerum dolorum doloremque.</p>
       ・・・中略・・・
    </div>
    <div class="linkTarget" id="section5">
      <h2>Section 5</h2>
      <p>Lorem ipsum dolor sit amet, ...voluptatum animi maxime.</p>
       ・・・中略・・・
    </div>
  </main>
</div>
<div class="footer"></div><!--フッターにある程度の高さを指定-->
<div id="scroll-to-top"><!--トップへスクロールするボタン-->
  <p>Top</p>
</div>
.wrapper {
  max-width: 780px;
  margin: 20px auto 0;
  padding: 0 1rem;
}
.nav-wrapper {
  position: sticky;
  top: 0px;
  width: 100%;
  background-color: #fefefe;
  padding: .5rem 0;
}
.nav {
  width: 100%;
  max-width: 600px;
  display: flex;
  justify-content: space-between;
}
.contents {
  width: 100%;
}
.nav {
  padding-left: 0;
}
.nav li {
  list-style: none;
  color: #70B466;
}
.nav a {
  display: block;
  width: 100%;
  padding: .25rem .5rem;
  text-decoration: none;
  color: #70B466;
}
.nav a.active {
  color: #D0F0C1;
  background-color: #557E49;
}
.linkTarget {
  margin-top: 4rem;
}
.footer {
  width: 100vw;
  height: 400px;
  background-color: #eee;
  margin-top: 300px;
}
/*トップへスクロールするボタン*/
#scroll-to-top {
  position: fixed;
  right: 15px;
  bottom: 2rem;
  z-index: 100;
  font-size: 0.75rem;
  background-color: #557E49;
  width: 80px;
  height: 50px;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
  color: #fff;
  line-height: 50px;
  text-align: center;
  transition: opacity .4s;
  opacity: .7;
  cursor: pointer;
}
#scroll-to-top:hover {
  opacity: 1;
}

オプションの rootMargin に '-20% 0px -80%' を指定して、交差を判定する境界を画面の上から20%の位置にしています。どの位置を境界とするかはそのページの作りによります。中央を境界とする場合は、'-50% 0px -50%' または '-50% 0px' と指定します。margin の指定同様ショートハンドが使えます。

threshold は省略していますが、指定する場合は 0 を指定します(それ以外の値では機能しません)。

コールバック関数では、交差を検知をしたら、その要素(entry.target)を引数として別途定義してある関数 highlightNavMenu() を呼び出します。

関数 highlightNavMenu() では、引数で渡された要素の id 属性からリンク先(href 属性)の値を生成し、その値の href 属性を持つメニューの要素を取得します(45行目)。

そしてその要素に active クラスを追加して、ハイライト表示します。

highlightNavMenu() の前半は、すでに active クラスが追加されている要素から active クラスを削除してリセットしています。

51行目以降は、requestAnimationFrame() を使ったスムーススクロールの設定で IntersectionObserver とは直接関係はありません(関連項目:requestAnimationFrame を使ったスムーススクロール

※実際に使用する場合は、オプションやコールバック関数、オブザーバーなどの変数名が他と競合しないように書き換える必要があるかも知れません。

以下は上記スクロールスパイの部分を関数として定義する例です。実際に使用する HTML 構造などに合わせて、引数などを変更したり工夫する必要があるかと思います。

/* 
ioScrollSpy() には以下の引数を指定します。

rootSelector:監視対象の領域のセレクタ(デフォルトは null でビューポート全体)
rootMargin :交差する境界を指定する rootMargin オプションに指定する値。デフォルトは '-25% 0px -75%'
targetClass :監視対象の要素のクラス名(ドットは不要)。デフォルトは 'target'
navSelector:メニュー項目の要素のセレクタ(querySelector に指定する値)デフォルトは '.nav'
activeClass:現在交差しているアクティブな要素に指定するクラス名。 でふぉるとは 'active'
*/
  
const ioScrollSpy = (rootSelector=null, rootMargin='-25% 0px -75%', targetClass='target', navSelector='.nav', activeClass='active') => {
  
  //監視対象の要素を取得
  const targets = document.querySelectorAll('.' + targetClass);
  
  //オプション
  const options = {
    root: rootSelector ===null ? null : document.querySelector(rootSelector),
    //上から25%の位置を交差の境界とする
    rootMargin: rootMargin, 
    threshold: 0 //デフォルトなので省略可能
  }
  //コールバック関数
  const callback = (entries) => {
    // 交差を検知をしたら
    entries.forEach(entry => {
      //監視対象の要素が領域(境界)と交差していれば(isIntersecting が true の場合)
      if (entry.isIntersecting) {
        //その要素を引数として以下の関数を呼び出してメニューをハイライト
        highlightNavMenu(entry.target);
      }
    });
  }
  //オブザーバーを生成
  const observer = new IntersectionObserver(callback, options);
  
  //それぞれの監視対象の要素を observe() に指定して監視
  targets.forEach( (target) => {
    observer.observe(target);
  });
  //メニュー項目をハイライトする(色を変える)関数 
  const highlightNavMenu = (target) => {
    //現在アクティブになっている(active クラスが付与されている)メニューの要素を取得
    const highlightedMenu = document.querySelector(navSelector + ' ' + '.' + activeClass);
    //上記で取得した要素が存在すれば、その要素から active クラスを削除
    if (highlightedMenu !== null) {
      highlightedMenu.classList.remove(activeClass);
    }
    //引数で渡された現在交差している(isIntersecting が true の)要素の id 属性からリンク先(href)の値を生成
    const href = '#' + target.id;
    //上記で生成したリンク先を持つメニューが、現在交差している要素のリンク
    const currentActiveMenu =  document.querySelector(`a[href='${href}']`);
    //現在交差している要素のリンクに active クラスを追加
    currentActiveMenu.classList.add(activeClass);
  }
}
//上記で定義した関数に引数を指定して呼び出す
ioScrollSpy(null, '-25% 0px -75%','linkTarget', '#navigation');

以下のサイトを参考にさせていただきました。

JSでのスクロール連動エフェクトにはIntersection Observerが便利

オプション options

コンストラクタ IntersectionObserver() の第2パラメータには、オブザーバー(IntersectionObserver オブジェクト)をカスタマイズするためのオプションオブジェクトを指定することができます。

オプションを省略した場合はデフォルトが適用されます。

const options = {
  //以下は全てデフォルトを指定しているので省略した場合と同じ
  root: null, 
  rootMargin: '0px 0px 0px 0px',
  threshold: 0
}
 
//引数にコールバック関数とオプションを指定してオブザーバーを生成
const observer = new IntersectionObserver(callback, options); 

options には以下を指定することができます。

オプション 説明
root オブザーバーが監視する領域(DOM 要素)を指定します。省略または null を指定した場合(デフォルト)はブラウザのビューポートが監視対象の領域になります。
rootMargin 交差を計算するときに root のサイズを調整する値(root からの距離)をピクセルまたはパーセントで指定します。このオプションで指定した値を足した領域が交差の計算(検出)の基準となります。デフォルトは '0px 0px 0px 0px'
threshold 交差の閾値。コールバック関数を呼び出す際のタイミングを交差の割合で指定します。指定する値は、単一の数値か、0.0 と 1.0 の間の数値の配列で指定。デフォルトは 0。

root

root は監視する対象の領域で、root の境界ボックス(バウンディングボックス)が交差を検知する基準となります。※ 「境界ボックス」は要素とその子孫を完全に囲む可能な最小の矩形のことです。

root に要素を指定することで、その要素を監視対象の領域とすることができます。

省略または null を指定した場合(デフォルト)はブラウザのビューポートが監視対象の領域になります。

//オプション  
const options = {
  //監視対象の領域(wrapper クラスを指定した要素)を指定
  root: document.querySelector('.wrapper'),
}
 
//コールバック関数
const callback = (entries) => {
  //コールバック関数の処理
}
 
//オプションを指定してオブザーバーを生成
const observer = new IntersectionObserver(callback, options);

監視対象の領域(root)はデフォルト(ブラウザのビューポート)の場合も、特定の要素を指定した場合も、矩形の領域になり、rootMargin で調整することができます。

rootMargin

rootMargin は交差状態(対象の要素が領域に入った状態)を計算するときに root の境界ボックスに適用されるオフセット(root からの距離)です。rootMargin を指定することで root の領域を調整することができます。

言い換えると、このオプションで指定した値を足した領域(境界ボックス)が計算の基準となります。

例えば正の値を指定すると、実際に見える前に交差していると判定させることができ、負の値を指定すると、ある程度見えてから交差したと判定させることができます。

正の値の rootMargin を適用して拡大させた監視領域 root rootMargin rootMargin rootMargin 交差判定の境界 ( rootMargin により拡大) 交差している 対象の要素

指定方法は CSS の margin プロパティに似た書式で指定できます(ショートハンドで指定できます)。

時計回り(top, right, bottom, left)で指定する場合の例 :'10px 20px 30px 40px'

それぞれのオフセットは px または % で指定します。デフォルトは '0px 0px 0px 0px' または '0px'。

rootMargin には単位が必要

単位( px または % )を付ける必要があります。

単位を付けないと「Uncaught DOMException: Failed to construct 'IntersectionObserver': rootMargin must be specified in pixels or percent.」のようなエラーになります。

以下は root の周囲を50px分拡大させる場合の指定例です。

const options = {
  //実際に見えるより 50px 前で交差していると判定させる(root の周囲を50px拡大)
  rootMargin: '50px'
}
 
const observer = new IntersectionObserver(callback, options);  

rootMargin の値は交差する root の境界ボックスの各辺にオフセットを追加定義して、最終的な交差の root の境界を作成します。

最終的な境界はコールバック実行時に IntersectionObserverEntry オブジェクト(各 entry)の rootBounds プロパティで取得することができます。

境界で判定(領域を線に)

監視対象の領域は高さを持つ面(矩形)ですが、rootMargin の指定で高さをが0の領域(境界線)とすることができます。

高さを持つ矩形ではなく、境界線を交差したかどうかで判定する方が便利な場合もあります。

但し、この場合、threshold にはデフォルトの 0 (省略可能)を指定します(それ以外の値ではコールバック関数は呼び出されません)。

領域の中央を交差の境界にする

監視領域の垂直方向中央を交差判定の境界とするには、rootMargin: '-50% 0px' とすれば良いようです(通常のマージンは%で指定すると包含ブロックの幅に対するパーセント値になりますが)。

root(監視領域) rootMargin: -50% 0 rootMargin: -50% 0 交差判定の境界 対象の要素 (交差していない)
const options = {
  //監視する対象の領域(省略すれば、ブラウザ画面が対象の領域)
  root: document.querySelector('.wrapper'),
  //垂直方向の中央を交差判定の境界とする
  rootMargin: '-50% 0px', 
  //threshold には 0 を指定(省略可能)
  threshold: 0
}

rootMargin に '-50% 0px' を指定すると rootMargin を適用した領域(rootBounds)の高さ(entry.rootBounds.height)は 0 になり、位置はroot の垂直方向の中央になります(entry.rootBounds.top と entry.rootBounds.bottom は同じ値になります)。

コールバック関数の中で、監視している領域(root)の DOMRectReadOnly を返す rootBounds の height や top、bottom の値で確認することができます。

const options = {
  root: document.querySelector('.wrapper'),
  //垂直方向の中央を交差判定の境界とする
  rootMargin: '-50% 0px', 
  threshold: 0 //省略可能
}

const callback = (entries) => {
  entries.forEach( entry => {
    //コールバック関数が呼び出される際に rootBounds の値をコンソールに出力
    console.log('top: ' + entry.rootBounds.top);  //垂直方向の中央
    console.log('bottom: ' + entry.rootBounds.bottom);  //垂直方向の中央
    console.log('height: ' + entry.rootBounds.height); // 0
    console.log('width: ' + entry.rootBounds.width);
  });
}

以下は、垂直方向中央を交差判定の境界としたサンプルです。領域の垂直方向中央に交差するとコールバック関数が呼び出されて、カウントアップします(初期状態ではカウントは1になっています)。

//コンストラクタに渡すオプション  
const options = {
  root: document.querySelector('.wrapper'),
  rootMargin: '-50% 0px', 
  threshold: 0 //省略可能
}
//カウント
let count = 0;
//コールバック関数
const callback = (entries) => {
  entries.forEach( entry => {
    //コールバック関数が呼び出されたらカウントアップ
    count++;
    countSpan.textContent = count;
    /* //確認用の出力
    console.log('top: ' + entry.rootBounds.top);
    console.log('bottom: ' + entry.rootBounds.bottom);
    console.log('height: ' + entry.rootBounds.height);
    console.log('width: ' + entry.rootBounds.width);*/
  });
}
 
//オブザーバーを生成
const observer = new IntersectionObserver(callback, options);
 
//監視する対象の要素(target クラスを指定した要素)を取得
const target = document.querySelector('.target');
  
//対象の要素を observe() メソッドに指定して監視
observer.observe(target);
 
//カウントの出力先の span 要素
const countSpan =  document.querySelector('#count span');
<div class="wrapper">
  <div class="sample"></div>
  <div id="target">監視対象の要素</div>
  <div class="sample"></div>
</div>
<p id="count">count(呼び出し回数): <span></span></p>
rootMargin: -50% 0 50% 50%
監視対象の要素

count(呼び出し回数):

領域の上から25%を交差の境界にする

rootMargin に '-25% 0px -75%' を指定すると、前述の例と同様、 rootMargin を適用した領域(rootBounds)の高さ(entry.rootBounds.height)は 0 になります。また、位置(top と bottom)は root の上から25%になります。

以下は、上から25%の位置を交差判定の境界としたサンプルです。領域の25%の位置に交差するとコールバック関数が呼び出されて、カウントアップします(初期状態ではカウントは1になっています)。

const options = {
  root: document.querySelector('.wrapper'),
  rootMargin: '-25% 0px -75%', 
  threshold: 0 //省略可能
}
let count = 0;
const callback = (entries) => {
  entries.forEach( entry => {
    count++;
    countSpan.textContent = count;
  });
}
const observer = new IntersectionObserver(callback, options);
const target = document.getElementById('target');
observer.observe(target);

//カウントの出力先の span 要素
const countSpan =  document.querySelector('#count span');

//カウントをリセットする処理
document.querySelector('#resetCount').addEventListener('click', () => {
  count = 0;
  countSpan.textContent = 0;
}); 
<div class="wrapper">
  <div class="sample"></div>
  <div id="target">監視対象の要素</div>
  <div class="sample"></div>
</div>
rootMargin: -25% 0px -75% 25% 75%
監視対象の要素

count(呼び出し回数):

<div style="position: relative;">
  <svg width="300" height="200" viewBox="0 0 300 200" style="position: absolute; pointer-events: none;">
    <defs>
      <marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" fill="orange" orient="auto-start-reverse">
        <path d="M 0 0 L 10 5 L 0 10 z"></path>
      </marker>
    </defs>
    <line x1="0" y1="50" x2="300" y2="50" stroke="tomato" stroke-width="1" stroke-dasharray="6 3"/>
    <text x="60" y="40" font-size="12" fill="tomato">rootMargin: -25% 0px -75%</text>
    <polyline points="255,5 255,45" fill="none" stroke="orange" marker-start="url(#arrow)" marker-end="url(#arrow)"></polyline>
    <text x="260" y="28" font-size="12" fill="orange">25%</text>
    <polyline points="255,55 255,195" fill="none" stroke="orange" marker-start="url(#arrow)" marker-end="url(#arrow)"></polyline>
    <text x="260" y="130" font-size="12" fill="orange">75%</text>
  </svg>
  <div class="wrapper">
    <div class="sample"></div>
    <div id="target">監視対象の要素</div>
    <div class="sample"></div>
  </div>
</div>
<p id="count" >count(呼び出し回数): <span></span></p>
<div>
  <button type="button" id="resetCount">Reset Count</button>
</div>

threshold

threshold は交差の閾値で、コールバック関数を呼び出すタイミングを交差の割合で指定することができます。指定する値は 0.0 と 1.0 の間の単一の数値か、0.0 と 1.0 の間の数値の配列で指定します。

交差の割合が閾値を上回るか下回った時(閾値に達した時と離れる時)にコールバック関数が呼び出されて実行されます。

デフォルトは 0 です。0 の場合は監視対象の要素が1ピクセルでも表示されるとコールバック関数が呼び出され、交差が終了する(0になる)時にも呼び出されます。つまり、対象の要素が見え始める時と見えなくなる時にコールバック関数が呼び出されます。

1と指定すると、監視対象の要素全体が表示されまでコールバック関数が呼び出されません。全体が表示された(交差の割合が1になった)際と交差の割合が1から変わった時にコールバック関数が呼び出されます。

1と指定した場合、監視対象が監視領域より大きいと全体が表示されることはない(交差の割合は1未満になる)のでコールバック関数は呼び出されません。

[ 0, 0.5, 1 ] と指定すると、監視対象の要素が表示されたとき、半分表示された時、全部表示されたとき、及びそれらの割合から外れる際にコールバック関数が呼び出されます。

以下は threshold に異なる値を指定した場合の、コールバック関数の呼び出しを確認するサンプルです。

コールバック関数が呼び出されると、その時点での交差の割合(intersectionRatio)を出力します。

呼び出されるたびにカウントを増やしています。Reset Count ボタンをクリックするとカウントを 0 にリセットします。

この例の場合、初期状態ではすでにコールバック関数が呼び出されているので1になっています(isIntersecting で判定すれば、初期状態で0にすることもできます)。

threshold : 0

以下は threshold に 0 を指定した場合の例です。

対象の要素が見え始める時と見えなくなる時にコールバック関数が呼び出されます。

const options = {
  threshold: 0 //デフォルト(省略時と同じ)
}

出力される交差の割合(intersectionRatio)は必ずしも0になるとは限りません(ゆっくりスクロールすると0になります)。

intersectionRatio:

count(呼び出し回数):

監視対象の要素
//オプション  
const options = {
  //監視領域
  root: document.querySelector('.wrapper'),
  //閾値
  threshold: 0 
}
//カウント
let count = 0;
 
//コールバック関数
const callback = (entries) => {
  entries.forEach( entry => {
    //呼び出されたらウントを増加
    count++;
    //intersectionRatio の値とカウントを出力
    ratioSpan.textContent = entry.intersectionRatio;
    countSpan.textContent = count;
  });
}
 
//オブザーバーを生成
const observer = new IntersectionObserver(callback, options);
 
//監視する対象の要素(target クラスを指定した要素)を取得
const target = document.querySelector('.target');
  
//対象の要素を observe() メソッドに指定して監視
observer.observe(target);
 
//出力先の span 要素
const ratioSpan =  document.querySelector('#ratio span');
const countSpan =  document.querySelector('#count span');
  
//カウントをリセットするイベント
document.querySelector('#resetCount').addEventListener('click', () => {
  count = 0;
  countSpan.textContent = 0;
});
<p id="ratio" class="status">intersectionRatio: <span></span></p>
<p id="count" class="status">count(呼び出し回数): <span></span></p>
<div class="wrapper">
  <div class="sample"></div>
  <div class="target">監視対象の要素</div>
  <div class="sample"></div>
</div>
<button type="button" id="resetCount">Reset Count</button>
/*監視対象の領域*/
.wrapper { 
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}
.sample {
  height: 300px;
}
/*監視対象の要素*/
.target {
  width: 200px;
  height: 160px;
  background-color: darkseagreen;
  margin: 0 auto;
  text-align: center;
  line-height: 160px;
  color: #fff;
} 

threshold :1

以下は threshold に 1 を指定した場合の例です。

全体が表示された時と交差の割合が1から変わる(全体が表示されなくなる)時にコールバック関数が呼び出されます。

const options = {
  threshold: 1 
}

チェックボックスにチェックを入れると対象の要素の高さを監視する領域の高さより大きな値に変更します(その場合、交差の割合は1にならないのでコールバック関数は呼び出されません)。

intersectionRatio:

count(呼び出し回数):

監視対象の要素

threshold に配列を指定

以下は threshold に配列(単一または複数の 0.0 と 1.0 の間の数値)を指定した場合の例です。

例えば [0, 0.5, 1] を指定すると、要素が表示されたとき、半分表示された時、全部表示されたときとそれらの状態から変わる時にコールバック関数が呼び出されます。

対象要素の高さによってはコールバック関数が呼び出されない場合があります。

以下の場合、監視する領域の高さは 200px なので、対象の要素の高さが 400px であれば、閾値が 0.5 未満の場合にしかコールバック関数は呼び出されません。

const options = {
  threshold: [0, 0.5, 1]
}

intersectionRatio:

count(呼び出し回数):

監視対象の要素

以下のセレクトボックスで閾値を変更できます。

以下のスライダーで対象要素の高さを変更できます。

target height: 160
<p id="ratio">intersectionRatio: <span></span></p>
<p id="count">count(呼び出し回数): <span></span></p>
<div class="wrapper">
  <div class="sample"></div>
  <div class="target">監視対象の要素</div>
  <div class="sample"></div>
</div>
<div>
  <select name="threshold">
    <option value="0.5">[0.5] </option>
    <option value="0,1">[ 0, 1 ]</option>
    <option value="0, 0.2">[ 0, 0.2 ]</option>
    <option value="0, 0.5, 1" selected>[ 0, 0.5, 1 ]</option>
    <option value="0, 0.25, 0.5, 0.75, 1">[ 0, 0.25, 0.5, 0.75, 1 ]</option>
    <option value="0, 0.3, 0.8">[ 0, 0.3, 0.8 ]</option>
  </select>
</div>
<div>
  <button type="button" id="resetCount">Reset Count</button>
</div>
<div style="display:flex">
  <span>target height:</span>
  <input type="range" name="height" min="100" max="600" value="160" step="10">
  <span id="targetHeight">160</span> 
</div>
//オブザーバーを格納する変数
let myObserver;  
//対象の要素を 格納する変数
let target;
//カウント
let count = 0;
//select 要素を取得
const selectElem = document.querySelector('select[name="threshold"]');
//閾値に指定する値(セレクトボックスの選択されている値を配列に変換)
let threshold = selectElem.value.split(',').map( value => parseFloat(value) );
//セレクトボックスのイベント(変更された場合の処理)
selectElem.addEventListener('change', (e) => {
  const val = e.currentTarget.value;
  //オブザーバーによる表示状態の変化の監視を停止(閾値の設定をクリア)
  myObserver.disconnect();  //または myObserver.unobserve(target);
  //オブザーバーを初期化
  myObserver = null;
  //閾値に指定する値(セレクトボックスの選択されている値を配列に変換)
  threshold = val.split(',').map( value => parseFloat(value) );
  //オブザーバーを生成する関数の呼び出し
  setObserver();
  //カウントをリセット
  count = 0;
  //出力されている intersectionRatio の値を空に
  ratioSpan.textContent = '';
}); 
  
//オブザーバーを生成する関数
const setObserver = () => {
  //オプション  
  const options = {
    //監視領域
    root: document.querySelector('.wrapper'),
    //閾値
    threshold: threshold
  }
  //コールバック関数
  const callback = (entries) => {
    entries.forEach( entry => {
      //呼び出されたらウントを増加
      count++;
      //intersectionRatio の値とカウントを出力
      ratioSpan.textContent = entry.intersectionRatio;
      countSpan.textContent = count;
    });
  }
  //オブザーバーを生成
  myObserver = new IntersectionObserver(callback, options);
  //監視する対象の要素(target クラスを指定した要素)を取得
  target = document.querySelector('.target');
  //対象の要素を observe() メソッドに指定して監視
  myObserver.observe(target);
}
//オブザーバーを生成する関数の呼び出し
setObserver();
//出力先の span 要素
const ratioSpan =  document.querySelector('#ratio span');
const countSpan =  document.querySelector('#count span');
//カウントをリセットするイベント
document.querySelector('#resetCount').addEventListener('click', () => {
  count = 0;
  countSpan.textContent = 0;
});
//スライダーの値の出力先の要素
const targetHeight = document.getElementById('targetHeight');
//スライダーのイベント
document.querySelector('input[name="height"]').addEventListener('input', (e) => {
  const targetHeightValue = e.currentTarget.value;
  targetHeight.textContent = targetHeightValue;
  target.style.setProperty('height', targetHeightValue + 'px');
  target.style.setProperty('line-height', targetHeightValue + 'px');
});
threshold を生成する関数

多数の閾値を手動で作成(記述)するのは大変なので、例えば以下のような関数を作成すると簡単に等間隔の threshold の値を設定することができます。

閾値に1を含める場合は、第2引数に true を指定します(省略した場合は含めません)。

/* count で指定した数の threshold の値を返す関数
count : 閾値の数(2以上の整数)
one: 1 を閾値として含めるかどうかの真偽値(デフォルトは false で含めない)*/
const makeThresholdArray = (count, one=false) => {
  //閾値 の値を格納する配列
  const threshold = [];
  for(let i = 0; i < count; i++) {
    if(count > 1) {
      //インデックスを総数(count)で割った値を閾値として配列に追加
      threshold.push(i/count);
    }
  }
  //第2引数の one が true の場合、最後に 1 を追加
  if(one) threshold.push(1);
  return threshold;
}
  
const options = {
  //閾値のリスト(0.01 刻みの100個の値)
  threshold: makeThresholdArray(100) 
  //100個の要素の配列を指定 [0,0.01,0.02,0.03,0.04,0.05, … 0.96,0.97,0.98,0.99]
}

コールバック関数 callback

コールバック関数は IntersectionObserverEntry オブジェクトの配列(entries)とコンストラクタで生成される IntersectionObserver オブジェクト自身(observer)を引数に受け取ります。

コールバック関数が受け取る引数
引数 説明
entries IntersectionObserverEntry オブジェクトの配列。
observer このコールバック関数を呼び出す IntersectionObserver オブジェクト(オブザーバー)
//entries は IntersectionObserverEntry オブジェクトの配列
const callback = (entries, observer) => {
  //entry は IntersectionObserverEntry オブジェクト
  entries.forEach( entry => {
    //監視対象の要素は entry.target
  });
}

コールバック関数の中では、監視対象の要素に target プロパティでアクセスすることができます。

その他に、対象の要素の交差している割合やその時点で交差しているかどうかなどの情報を IntersectionObserverEntry オブジェクトから取得することができます。

また、コールバック関数の中では、第2引数のオブザーバー(IntersectionObserver )のメソッドを使うことができます。

isIntersecting

isIntersecting は IntersectionObserverEntry のプロパティで、監視対象の要素が、監視している領域に入ったら(交差したら) true を、入っていなければ(交差していなければ) false を返す真偽値です。

isIntersecting を使うことで、監視対象の要素が監視している領域に入っている場合にのみ処理を実行することができます。

例えば、以下の場合、監視対象の要素が監視している領域に入っている場合にのみ true とコンソールに出力します。

const callback = (entries, observer) => {
  entries.forEach( entry => {
    //監視対象の要素が監視している領域に入っている場合にのみ実行
    if (entry.isIntersecting) {
      console.log(entry.isIntersecting);
    }
  });
}

以下の場合は、交差状態に関わらずコールバック関数が呼び出されたら実行されるので、初期状態で対象の要素が領域内になければ即座に false とコンソールに出力されます。

const callback = (entries, observer) => {
  entries.forEach( entry => {
    //以下は交差状態に関わらずコールバック関数が呼び出されたら実行される
    console.log(entry.isIntersecting);
  });
}

以下のサンプルは、チェックボックスにチェックを入れると(初期状態では)コールバック関数が呼び出される際に isIntersecting を使って監視対象の要素が領域に入っている場合にのみ、カウントアップします。

チェックを外すと、交差の状態に関わらず(閾値に達した時と離れる時)にカウントアップします。

const callback = (entries) => {
  entries.forEach( entry => { 
    //呼び出されて対象の要素が領域に入っていれば(交差していれば)
    if (entry.isIntersecting) {
      //カウントアップ(カウントを1増加)
      countSpan.textContent = ++count;
    }
    //呼び出されれば交差の状態に関わらずカウントアップ(カウントを1増加)
    countSpan.textContent = ++count; 
  });
}

Count: 0

監視対象の要素
threshold : 0

intersectionRatio

intersectionRatio は IntersectionObserverEntry のプロパティで、監視対象の要素が領域と交差している割合(どのぐらい見えているか、交差しているか)を 0.0 〜 1.0 の間の数値で返します。

以下は監視対象の要素が領域に入ったら、threshold で指定したタイミングで監視対象の要素が領域と交差している割合を検出して、その値により背景色を変更する例です。

//コールバック関数
const callback = (entries, observer) => {
  entries.forEach( entry => {
    //監視対象の要素が領域と交差している場合の処理
    if (entry.isIntersecting) {
      //監視対象の要素が領域と交差している割合
      const ratio = entry.intersectionRatio;
      //交差している割合を出力(確認用)
      ratioSpan.textContent = ratio;
      //交差している割合(threshold に対応)により背景色を変更
      if(ratio >= 0 && ratio < .25) {
        entry.target.style.backgroundColor = 'cornflowerblue';
      }else if(ratio >= .25 && ratio < .5) {
        entry.target.style.backgroundColor = 'darkseagreen';
      }else if(ratio >= .5 && ratio < .75) {
        entry.target.style.backgroundColor = 'orange';
      }else{
        entry.target.style.backgroundColor = 'palevioletred';
      }
    } 
  });
}

//オプション  
const options = {
  //監視対象の領域(wrapper クラスを指定した要素)を指定
  root: document.querySelector('.wrapper'),
  //閾値(コールバック関数を呼び出すタイミング)のリスト
  threshold: [0, 0.25, 0.5, 0.75]
}

//オブザーバーを生成
myObserver = new IntersectionObserver(callback, options);

//監視対象の要素を取得
const target = document.querySelector('.target');

//オブザーバーに監視対象の要素を指定 
myObserver.observe(target);
  
//交差している割合を出力する要素(確認用)
const ratioSpan = document.querySelector('#ratio span');
  
//スライダーのイベント
document.querySelector('input[name="height"]').addEventListener('input', (e) => {
  const targetHeightValue = e.currentTarget.value;
  target.style.setProperty('height', targetHeightValue + 'px');
  target.style.setProperty('line-height', targetHeightValue + 'px');
}); 
<div class="wrapper">
  <div class="target">監視対象の要素</div>
</div>
<p id="ratio">intersectionRatio: <span></span></p>
<input type="range" name="height" min="100" max="600" value="160" step="10">

CSS ではトランジションを背景色に設定しています。

.wrapper {
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}
/*監視対象の要素*/
.target {
  width: 160px;
  height: 180px;
  margin: 200px auto;
  color: #fff;
  text-align: center;
  line-height: 200px;
  background-color: cornflowerblue;
  transition: background-color 1s; /*トランジション*/
}

ゆっくりスクロールすると対象の要素の見える割合によりその背景色が変化します。

但し、対象の要素の高さが領域の高さよりある程度以上大きいと、要素の見える割合が限定されます(例えば、対象の要素の高さが領域の高さの2倍あると割合は最大で 0.5 になります)。

また、threshold は [0, 0.25, 0.5, 0.75] と指定していますが、実際にコールバック関数が呼び出された際の割合(intersectionRatio)は正確に一致するわけではないのが確認できます(ゆっくりスクロールすると一致する場合が多いです)。

監視対象の要素

intersectionRatio:

target height: 160

以下は監視する領域に対象の要素が入ったら、2つの対象の要素を intersectionRatio の値を使ってアニメーションする例です。

対象の要素には、そのクラス属性の値(entry.target.classList)を判定して異なるアニメーションを設定しています。また、アニメーションがスムースになるように閾値(threshold)に 0.01 刻みの100個の値を指定しています。

//コールバック関数
const callback = (entries, observer) => {
  //entry は IntersectionObserverEntry オブジェクト
  entries.forEach( entry => {
    //監視対象の要素が監視している領域に入っていれば
    if (entry.isIntersecting) {
      //クラスに blue が含まれていれば
      if(entry.target.classList.contains('blue')) {
        //intersectionRatio の値を使って scale() に指定する値を作成
        const scale = 0.3 + 0.7 * entry.intersectionRatio;
        //transform に scale() を設定
        entry.target.style.setProperty('transform', `scale(${scale})`);
        //クラスに red が含まれていれば
      }else if(entry.target.classList.contains('red')) {
        //opacity の値に intersectionRatio の値を設定
        entry.target.style.setProperty('opacity', entry.intersectionRatio);
      }
    } 
  });
}

//count で指定した数の threshold の値を返す関数
const makeThresholdArray = (count) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    threshold.push(i/count);
  }
  return threshold;
}

//オプション  
const options = {
  //監視対象の領域(wrapper クラスを指定した要素)を指定
  root: document.querySelector('.wrapper'),
  //閾値のリスト(0.01 刻みの100個の値)
  threshold: makeThresholdArray(100) 
}

//オブザーバーを生成
myObserver = new IntersectionObserver(callback, options);

//監視する対象の要素(target クラスを指定した要素)を全て取得
const targets = document.querySelectorAll('.target');

//全ての対象の要素に対して
targets.forEach( (elem) => {
  //observe() メソッドに監視対象の要素を指定
  myObserver.observe(elem);
});  
<div class="wrapper">
  <div class="target blue">監視対象の要素1</div>
  <div class="target red">監視対象の要素2</div>
</div>
/*監視する領域の要素*/
.wrapper {
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}

/*監視対象の要素*/
.target {
  width: 160px;
  height: 200px;
  margin: 200px auto;
  color: #fff;
  text-align: center;
  line-height: 200px;
}
.target.blue {
  background-color: cornflowerblue;
} 
.target.red {
  background-color: palevioletred;
} 
監視対象の要素1
監視対象の要素2

IntersectionObserverEntry

IntersectionObserverEntry オブジェクトは、監視領域(root)と監視対象の要素(ターゲット要素)の交差状態を表すオブジェクトです。

IntersectionObserverEntry には以下のようなプロパティがあります。メソッドはありません。

監視対象の要素が監視している領域に交差しているかの真偽値をを返す isIntersecting や監視対象の要素が領域と交差している割合を返す intersectionRatio、ターゲット要素(監視対象の DOM 要素)を返す target などがあります。

また、以下のプロパティから監視対象の要素及び監視している領域のサイズや位置などの情報を取得することができます。

プロパティ 説明
boundingClientRect ターゲット要素(監視対象の要素)全体の矩形の境界を DOMRectReadOnly として返します。getBoundingClientRect() で取得するオブジェクトと同じで、ターゲット要素の各プロパティ(left, top, right, bottom, x, y, width, height)を参照できます。
intersectionRect ターゲット要素の表示領域(監視領域と交差している、つまり表示されている部分)を表す DOMRectReadOnly を返します。
intersectionRatio boundingClientRect(ターゲット要素全体)と intersectionRect(ターゲット要素の領域に入っている部分)の比率を返します。言い換えると、監視対象の要素が監視している領域と交差している割合を返します。返される値は 0.0 〜 1.0 の間の数値です。
isIntersecting ターゲット要素(監視対象の要素)が、監視している領域(root)に入ったら true を返す真偽値です。false の場合、ターゲット要素は監視している領域に交差していません(入っていない)。
rootBounds 監視している領域(root)の DOMRectReadOnly を返します。rootMargin が指定されている場合は、その分オフセットされます。
target ターゲット要素(監視対象の DOM 要素)を返します。
time ドキュメントの開始時点を基準にした交差が記録された時刻を示すタイムスタンプ DOMHighResTimeStamp (ミリ秒単位)

以下は threshold に0.01刻みの100個の閾値を指定して、コールバック関数が呼び出される際に IntersectionObserverEntry プロパティの値を出力する例です。

  • 監視する領域: height:200px / width:300px / border:1px
  • 監視対象の要素: height:200px / width:160px

例えば、intersectionRatio が 0.5 の時は intersectionRect.height(DOMRect の height プロパティ)は監視対象の要素の高さの半分(100)になります。

rootBounds(root の DOMRect)の width や height にはボーダーの値が含まれてません(この例の場合、root の height は 200px ですが、rootBounds.height は 198 になっています)。

監視対象の要素

ボックス内でスクロールすると対象の要素が現れます

プロパティ
boundingClientRect.height
boundingClientRect.top
intersectionRatio
intersectionRect.height
isIntersecting
rootBounds.height
rootBounds.top
target.id
time
target Top(※)

(※) target Top は entry.boundingClientRect.top - entry.rootBounds.top です(ターゲット要素のトップから root のトップを差し引いた値、ターゲット要素の領域のトップからの距離)。交差していない状態では更新されません。

この例では表に出力していますが、コンソールに出力すれば簡単です。

<div class="wrapper"><!--監視対象の領域(root)-->
  <div class="sample"></div>
  <div id="targetElem">監視対象の要素</div>
  <div class="sample"></div>
</div>

<table>
  <tr>
    <th>プロパティ</th>
    <th>値</th>
  </tr>
  <tr>
    <td>boundingClientRect.height</td>
    <td id="boundingClientRect"></td>
  </tr>
  <tr>
    <td>boundingClientRect.top</td>
    <td id="boundingClientRectTop"></td>
  </tr>
  <tr>
    <td>intersectionRatio</td>
    <td id="intersectionRatio"></td>
  </tr>
  <tr>
    <td>intersectionRect.height</td>
    <td id="intersectionRect"></td>
  </tr>
  <tr>
    <td>isIntersecting</td>
    <td id="isIntersecting"></td>
  </tr>
  <tr>
    <td>rootBounds.height</td>
    <td id="rootBounds"></td>
  </tr>
  <tr>
    <td>rootBounds.top</td>
    <td id="rootBoundsTop"></td>
  </tr>
  <tr>
    <td>target.id</td>
    <td id="target"></td>
  </tr>
  <tr>
    <td>time</td>
    <td id="time"></td>
  </tr>
  <tr>
    <td>target Top</td>
    <td id="targetTop"></td>
  </tr>
</table>
/*監視対象の領域*/
.wrapper {
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;  /*overflow に scroll を指定*/
}
.sample {
  height: 200px;
}
/*監視対象の要素*/
#targetElem {
  width: 160px;
  height: 200px;
  background-color: royalblue;
  margin: 300px auto;
  color: #fff;
  text-align: center;
  line-height: 200px;
}
//threshold の値を返す関数
const makeThresholdArray = (count) => {
  const threshold = [];
  for(let i = 0; i < count; i++) {
    threshold.push(i/count);
  }
  return threshold;
}
  
//コンストラクタに渡すオプション  
const options = {
  //監視対象の領域
  root: document.querySelector('.wrapper'),
  //閾値のリスト(0.01 刻みの100個の値)
  threshold: makeThresholdArray(100) 
}
 
//値の出力先の要素を取得
const boundingClientRect = document.getElementById('boundingClientRect');
const boundingClientRectTop = document.getElementById('boundingClientRectTop');
const intersectionRatio = document.getElementById('intersectionRatio');
const intersectionRect = document.getElementById('intersectionRect');
const isIntersecting = document.getElementById('isIntersecting');
const rootBounds = document.getElementById('rootBounds');
const rootBoundsTop = document.getElementById('rootBoundsTop');
const target = document.getElementById('target');
const time = document.getElementById('time');
const targetTop = document.getElementById('targetTop');
 
//コンストラクタに渡すコールバック関数
const callback = (entries) => {
  //entries は IntersectionObserverEntry オブジェクトの配列
  entries.forEach(entry => {
    //監視対象の要素が領域内に入った(isIntersecting が true の)場合の処理
    if (entry.isIntersecting) {
      boundingClientRect.textContent = entry.boundingClientRect.height;
      boundingClientRectTop.textContent = entry.boundingClientRect.top;
      intersectionRatio.textContent = entry.intersectionRatio;
      intersectionRect.textContent = entry.intersectionRect.height;
      isIntersecting.textContent = entry.isIntersecting;
      rootBounds.textContent = entry.rootBounds.height;
      rootBoundsTop.textContent = entry.rootBounds.top;
      target.textContent = entry.target.id;
      time.textContent = entry.time;
      targetTop.textContent =  entry.boundingClientRect.top - entry.rootBounds.top;
    } else {
      boundingClientRect.textContent = entry.boundingClientRect.height;
      boundingClientRectTop.textContent = entry.boundingClientRect.top;
      intersectionRatio.textContent = entry.intersectionRatio;
      intersectionRect.textContent = entry.intersectionRect.height;
      isIntersecting.textContent = entry.isIntersecting;
      rootBounds.textContent = entry.rootBounds.height;
      rootBoundsTop.textContent = entry.rootBounds.top;
      target.textContent = entry.target.id;
      time.textContent = entry.time;
      targetTop.textContent =  entry.boundingClientRect.top - entry.rootBounds.top;
    }
  })
}
 
//コンストラクタを使って IntersectionObserver オブジェクトを生成
const observer = new IntersectionObserver(callback, options);
 
//監視する対象の要素を取得
const targetElem = document.getElementById('targetElem');
  
//生成したオブジェクトを使って対象の要素を監視
observer.observe(targetElem);  

IntersectionObserver

IntersectionObserver オブジェクトには以下のようなプロパティとメソッドがあります。

プロパティ 説明
root 監視している領域の DOM 要素。コンストラクターのオプションに値が渡されなかった場合はこの値は null となり、最上位の文書のビューポートが使用されます。
rootMargin 交差状態(対象の要素が領域に入った状態)を計算するときに root のバウンディングボックス(境界ボックス)に適用されるオフセットの矩形。それぞれのオフセットはピクセル (px) またはパーセント値 (%) で表すことができます。コンストラクターのオプションに値が渡されなかった場合の既定値は "0px 0px 0px 0px"
thresholds この場合、threshold ではなく thresholds(複数形)。交差領域と監視対象のバウンディングボックス領域との比の閾値を昇順に並べた交差の閾値リスト。コンストラクターのオプションで値が渡されなかった場合の既定値は 0 。このプロパティで取得する値は、実際にコンストラクターのオプションに指定した値と微妙に異なるようです。
メソッド 説明
disconnect() IntersectionObserver オブジェクトが対象を監視することを停止します。オブザーバーに指定した対象の要素と閾値をクリアします。disconnect()
observe() 指定された要素(DOM)を監視します(監視対象に追加します)。
takeRecords() 前回交差をチェックした後で交差状態の変化があった対象要素を示す IntersectionObserverEntry オブジェクトの配列を返します。このメソッドを呼び出すと処理待ちの交差リストをクリアしてしまうため、コールバックが実行されません。
unobserve() 指定された要素(DOM)の監視を停止します。

unobserve() 停止

unobserve() メソッドを使って監視を停止することができます。

例えば、監視する領域に対象の要素が入ったら一度だけアニメーションを実行するには以下のように unobserve() を使って一度処理を実行したら監視を停止することができます。

unobserve() メソッドはオブザーバーのメソッドで、オブザーバーはコールバック関数の第2引数に渡されます。

この場合、監視対象の要素が初めて領域内に入った時にだけクラスが追加され、それ以降スクロールして監視対象の要素が領域内に入っても何も起こりません。

この例の場合、クラスを追加するだけなので(クラスを削除しないので)、unobserve() で停止しなくても一度しかアニメーションは実行されませんが、不要な監視を停止することができます。

複数の監視対象の要素があれば、それぞれの要素に対して一度だけ処理が実行され、対象の要素の監視が解除されます。

const callback = (entries, observer) => {
  entries.forEach( entry => {
    //監視対象の要素が領域内に入った場合
    if (entry.isIntersecting) {
      //fadeIn クラスを追加
      entry.target.classList.add('fadeIn');
      //第2引数の observer の unobserve() メソッドで各要素の監視を停止
      observer.unobserve(entry.target);
    } 
  });
}

const options = {
  //監視対象の領域を指定
  root: document.querySelector('.wrapper'),
}

//オブザーバーを生成
const myObserver = new IntersectionObserver(callback, options);

//監視する対象の要素を全て取得
const targets = document.querySelectorAll('.target');

//全ての対象の要素に対して
targets.forEach( (elem) => {
  //observe() メソッドに監視対象の要素を指定
  myObserver.observe(elem);
});
停止後 監視を再開

unobserve() メソッドを使って監視を停止した後に、監視を再開するには、再度 observe() メソッドに監視対象の要素を指定します。

以下は前述の例に、再開するためのボタンを追加した例です。

この例の場合、監視の際に対象の要素にクラスを追加しているので、監視を再開する前に対象の要素のクラスを元の状態に戻しています(これを行わないと監視を再開しても、アニメーションが実行されない)。

const callback = (entries, observer) => {
  entries.forEach( entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('fadeIn');
      //unobserve() メソッドで各要素の監視を停止
      observer.unobserve(entry.target);
    } 
  });
}
const options = {
  root: document.querySelector('.wrapper'),
}
const myObserver = new IntersectionObserver(callback, options);
const targets = document.querySelectorAll('.target');
targets.forEach( (elem) => {
  myObserver.observe(elem);
});

//以下が追加部分
document.getElementById('restartObserver').addEventListener('click', () => {
  //対象の要素のクラスをリセット(追加したクラスを削除)
  document.querySelectorAll('.target').forEach( (elem) => {
    elem.classList.remove('fadeIn');
  });
  targets.forEach( (elem) => {
    //observe() メソッドに監視対象の要素を指定して監視を再開
    myObserver.observe(elem);
  });
});
<div class="wrapper">
  <div class="target blue">監視対象の要素1</div>
  <div class="target red">監視対象の要素2</div>
</div>
<button type="button" id="restartObserver">Restart</button>
.wrapper {
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}
.target {
  width: 160px;
  height: 200px;
  margin: 200px auto;
  color: #fff;
  text-align: center;
  line-height: 200px;
  opacity: 0;
  transition: opacity 3s; /*トランジション*/
}
.target.blue {
  background-color: cornflowerblue;
} 
.target.red {
  background-color: palevioletred;
} 
.target.fadeIn {
  opacity: 1;
}

スクロールするとアニメーションが一度だけ実行されます。Restart をクリックすると監視を再開するので、またスクロールすると一度だけアニメーションが実行されます。

監視対象の要素1
監視対象の要素2

disconnect() オブザーバーのリセット

IntersectionObserver の disconnect() を使うと、オブザーバーに指定した対象の要素と閾値をクリアして対象の要素の監視を停止します。但し、その後監視を再開するには、再度オブザーバーを生成する必要があるようです。

通常はあまり使うことはないかも知れませんが、例えば、オブザーバーのオプションを変更する場合など、オブザーバーをリセットする際に利用することができそうです。

以下は3つの要素を監視して、変更可能な threshold に指定したタイミングでコールバック関数が呼び出されたらカウントするサンプルです。

オブザーバーは関数を定義して生成しています。threshold を変更する際は disconnect() でオブザーバーに指定した対象の要素と閾値をクリアして、変更された値を threshold に設定して関数を呼び出しオブザーバーを再度生成しています。

//オブザーバーを格納する変数
let myObserver;  
//対象の要素を格納する変数
let targets;
//カウント
let count = 0;
//オプションの threshold に指定する値を格納する変数
let threshold = 0;

//スライダーの値(閾値)の出力先の要素
const thresholdSpan = document.getElementById('thresholdSpan');

//スライダーのイベント(スライダーが変更された際の処理)
document.querySelector('input[name="threshold"]').addEventListener('input', (e) => {
  //スライダー(input 要素の value)の値を取得
  const thresholdValue = e.currentTarget.value;
  thresholdSpan.textContent = thresholdValue;
  //オブザーバーに指定した対象の要素と閾値をクリア
  myObserver.disconnect();
  //または disconnect() の代わりに各要素を unobserve()
  /*targets.forEach((target) => {
    myObserver.unobserve(target);
  });*/
  //オブザーバーを初期化
  myObserver = null;
  //閾値に指定する値(変更されたスライダーの値を閾値に設定)
  threshold = thresholdValue;
  //オブザーバーを生成
  setObserver();
  //カウントをリセット
  count = 0;
  countSpan.textContent = 0;
});  
  
//オブザーバーを生成する関数
const setObserver = () => {
  //オプション  
  const options = {
    //監視領域
    root: document.querySelector('.wrapper'),
    //閾値
    threshold: threshold
  }
  //コールバック関数
  const callback = (entries) => {
    entries.forEach( entry => {
      //呼び出されて対象の要素が領域内にあれば
      if (entry.isIntersecting) {
        //カウントを増加
        countSpan.textContent = ++count;
      }
    });
  }
  //オブザーバーを生成
  myObserver = new IntersectionObserver(callback, options);
  //監視する対象の要素(target クラスを指定した要素)を取得
  targets = document.querySelectorAll('.target');
  //対象の要素を observe() メソッドに指定して監視
  targets.forEach(target => {
    myObserver.observe(target);
  })
}
//オブザーバーを生成
setObserver();

//出力先の span 要素
const countSpan =  document.querySelector('#count span');

//カウントをリセットするイベント
document.querySelector('#resetCount').addEventListener('click', () => {
  count = 0;
  countSpan.textContent = 0;
}); 
<p id="count" class="status">Count: <span>0</span></p>
<div class="wrapper">
  <div class="target blue">Target 1</div>
  <div class="target red">Target 2</div>
  <div class="target green">Target 3</div>
</div>
<div>
  <button type="button" id="resetCount">Reset Count</button>
</div>
<div> 
  <span>threshold :</span>
  <input type="range" name="threshold" min="0" max="1" value="0" step="0.1">
  <span id="thresholdSpan">0</span> 
</div>
.wrapper {
  height: 200px;
  width: 300px;
  border: 1px solid #ccc;
  overflow: scroll;
}
/*監視対象の要素*/
.target {
  width: 160px;
  height: 160px;
  margin: 300px auto;
  color: #fff;
  text-align: center;
  line-height: 160px;
}
.target.blue {
  background-color: cornflowerblue;
}
.target.red {
  background-color: palevioletred;
}
.target.green {
  background-color: mediumseagreen;
}

Count: 0

Target 1
Target 2
Target 3
threshold : 0