Intersection Observer API

Intersection Observer API (交差監視 API) は、ターゲットとなる要素が、祖先要素もしくは文書の最上位のビューポートと交差する変更を非同期的に監視する方法を提供します。

従来、要素の可視性や、二つの要素間で互いに相対的な可視性を検出することは難しく、どの解決方法も不確実であり、ブラウザーやユーザーがアクセスするサイトの反応を鈍くする要因の一つとなっていました。ウェブが成熟していくにつれてこのような情報の必要性は高まっていきます。 Intersection (要素間交差) についての情報は下記のような理由から必要とされています。

  • ページがスクロールした際の画像やその他のコンテンツの遅延読み込み。
  • 「無限スクロール」をするウェブサイトを実装し、スクロールに従って次々とコンテンツを読み込んで、ユーザーがページの切り替えをせずに済むようにすること。
  • 広告費用を計算するための広告が表示されたかどうかのレポート。
  • ユーザーが見るかどうかによって、タスクを実行するかどうか、アニメーションを処理するかを決定すること。

これまで、要素間の交差を検出する実装をするには、 Element.getBoundingClientRect() のようなメソッドを呼び出すイベントハンドラーやループがあり、影響を受ける要素に対する情報を都度計算し集めることで構成されていました。このようなコードがメインスレッドで実行されると、いずれかはパフォーマンスの問題を引き起こす可能性があります。試しにサイトにテストとして読み込めば分かりますが、事態は完全に酷くなりえます。

ウェブページで無限スクロールを使用することを考えてみてください。ベンダーから提供されるライブラリを使用して、ページ全体に定期的に配置された広告を管理し、アニメーショングラフィックスを表示し、通知ボックスなどを描画するカスタムライブラリを使用します。これらのそれぞれには独自の Intersection を検出するためのルーチンがあり、すべてがメインスレッド上で実行されます。ウェブサイトの作者は、これが起こっていることを認識していないかもしれません。なぜなら、彼らは内部の働きについてほとんど知らずに2つのライブラリを使用しているからです。ユーザーがページをスクロールすると、スクロール処理中にこれらの Intersection の検出ルーチンが絶えず起動し、ユーザーはブラウザー、ウェブサイト、およびコンピュータにイライラさせられることになります。

Intersection Observer API を使用することで、監視したい要素が別の要素 (もしくはviewport) に入ってきたり出ていったりする時、まだ両要素が交差する量がある一定の量を満たす時、実行されるコールバック関数を登録するが出来ます。こういった方法を用いることで、この手の要素交差を監視するためにサイトはメインスレッド上で何もする必要がなくなり、ブラウザーは要素間交差の管理を最適化して自由に行えます。

Intersection Observer API を使用してできないものの1つは、重複するピクセル数または具体的なピクセル数です。ただし、「N%前後のどこかで交差する場合に何かしたい」という一般的な利用法はカバーされています。

Intersection observer 概念と使い方

Intersection Observer API を使用すると、ある要素が、これをターゲットと呼びますが、端末のビューポートまたは指定された要素 - API の目的からこれをルート要素もしくはルートと呼びます - と交差するたびに呼び出されるコールバックを構成することができます。通常は、要素の直近のスクロール可能な祖先、または、要素がスクロール可能な要素の子孫でない場合はビューポートに関する交差状態の変更を監視したいでしょう。ルート要素に関する交差を監視するには、 null を指定してください。

ビューポートとその他の要素のどちらがルートとして使用されていても、 API は同じように動作し、ターゲット要素の表示状態が変わってルートとの間で交差の量の期待値を通るたびに、提供したコールバック関数が実行されます。

ターゲット要素とそのルート要素の交差する度合いが交差率です。これはターゲット要素のパーセンテージを 0.0 から 1.0 の間の値で表現したものです。

Intersection obserer の作成

コンストラクターを呼び出して Intersection observer を作成し、閾値が一方向また他の方向に交差する度に実行されるコールバック関数を渡します。

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

1.0 の閾値は、 root オプションで指定された要素内でターゲットが100%表示された時にコールバックが呼び出されることを意味しています。

Intersection observer のオプション

IntersectionObserver() コンストラクタに渡された options オブジェクトは、オブザーバーのコールバックが呼び出される状況を制御し、以下のフィールドがあります:

root
ターゲットが見えるかどうかを確認するためのビューポートとして使用される要素です。指定されなかった場合、もしくは null の場合はデフォルトでブラウザーのビューポートが使用されます。
rootMargin
root の周りのマージンです。CSS margin プロパティに似た値を持つことができます。例えば、"10px 20px 30px 40px" (top, right, bottom, left) のようなものです。この値はパーセント値にすることができます。この一連の値は、交差を計算する前にルート要素の範囲のボックスの各辺を拡大または縮小させることができます。既定ではすべてゼロです。
threshold
単一の数値もしくは数値の配列で、オブザーバーのコールバックを実行するターゲットがどのくらいの割合で見えているかを示します。 50% 通過したときのみ検出する場合は値 0.5 を使用します。 25% を超える度にコールバックを実行する場合は、 [0, 0.25, 0.5, 0.75, 1] という配列を指定します。既定値は 0 です (つまり、1ピクセルでも表示されるとコールバックが実行されます)。1.0 の値は全てのピクセルが見えるようになるまで、閾値をまたいだとみなされないことを意味します。

監視される要素をターゲットにする

オブザーバーを作成した後は、監視するターゲット要素を与える必要があります。

var target = document.querySelector('#listItem');
observer.observe(target);

ターゲットが IntersectionObserver に指定された閾値を満たす度にコールバックが呼び出されます。コールバックは IntersectionObserverEntry オブジェクトのリストとオブザーバーを受け取ります。

let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

コールバックはメインスレッドで実行される点に注意してください。可能な限り早く動作する必要があります。もし時間を要する処理であるなら、 Window.requestIdleCallback() を使ったほうがいいでしょう。

また root オプションを指定した場合、target はルート要素の子要素でなければなりません。

交差の計算方法

Intersection Observer API によって考慮される領域は全て矩形です。不規則に整形された要素は、要素全てを囲む最小の矩形で占有しているとみなされます。同様に、要素の可視部分が矩形ではない場合、要素が交差する矩形は要素全ての可視部分を含む最小の矩形であると解釈されます。

IntersectionObserverEntry オブジェクトによって提供される様々なプロパティがどのように交差を表現しているかを知るともっと役に立つでしょう。

交差するルートと root margin

要素とその入れ物との交差を監視するには、入れ物をまずは知る必要があります。ここでの入れ物とは交差ルートもしくはルート要素です。これは監視される要素の親要素となる文書内の特定の要素になるか、文書のビューポートを入れ物として使用する際は null になるかいずれかになります。

ルート交差矩形はターゲットをチェックするために使用される矩形です。この矩形は次のように決まります。

  • 交差ルートが暗黙のルート (すなわち最上位の Document) である場合、ルート交差矩形はビューポートの矩形になります。
  • 交差ルートのあふれた部分が切り取られていた場合、ルート交差矩形はルート要素のコンテンツ領域になります。
  • それ以外の場合は、ルート交差矩形は交差ルートのクライアント矩形 (getBoundingClientRect() を呼び出して返されるもの) です。

交差するルートとして使用される矩形は、ルートマージン rootMarginIntersectionObserver の作成時に設定することで調整することが可能です。 rootMargin の値は交差するルートの境界線各辺にオフセット追加定義して、最終的な交差のルートの境界線を作成します (コールバックが実行された際には IntersectionObserverEntry.rootBounds で取得できるものです)。

閾値

Intersection Observer API はターゲット要素がどのくらい見えているのか微細な変化を全て知らせるのではなく、閾値 (thresholds) を使用します。オブザーバーを作成する際に、表示されるターゲット要素がどの程度見えているかのパーセンテージを表す1つ以上の数値を指定できます。API はこれらの閾値を超えて見えたかどうかの変更のみを知らせます。

例えば、ターゲット要素が25%見える度に通知を受けたい場合は、オブザーバーを作成する際の閾値のリストとして [0, 0.25, 0.5, 0.75, 1] という配列を指定します。変更の通知を受ける時にコールバック関数に渡された IntersectionObserverEntryisIntersecting プロパティの値をチェックすることで、変更が感知された方向 (つまり要素が見えたかどうかを) 判断することが出来ます。isIntersectingtrue であれば、ターゲットは閾値を超えて少なくとも見るようになったということですし、false であればターゲットは指定した閾値では表示されなくなったということです。

閾値の仕組みを感じ取るには、下のボックスをスクロールして見てください。その中にある各色のボックスには四隅全てにパーセンテージが表示されています。そのため、入れ物をスクロールする時にこれらのパーセンテージが変化することが分かります。各ボックスには異なる閾値がセットされています:

  • 最初のボックスは可視点の各パーセンテージ値がセットされています; つまりIntersectionObserver.thresholds の配列は [0.00, 0.01, 0.02, ..., 0.99, 1.00] となります。
  • 2つ目のボックスは50%を指定した一つの閾値しか持ちません。
  • 3つ目のボックスは10%見える毎の閾値を持っています (0%, 10%, 20%...)
  • 最後のボックスは25%の閾値です。

クリッピングと交差矩形

ブラウザーは次のように最終的な交差矩形を計算します。これはすべて行われることですが、交差がいつ発生するかを正確に把握するために、これらの手順を理解すると役立ちます。

  1. ターゲット要素の境界矩形 (つまり、要素を構成するすべてのコンポーネントの境界ボックスを完全に囲む最小の矩形) は、ターゲットに対して getBoundingClientRect() を呼び出すことによって取得されます。これは、交差する矩形の最大の大きさです。残りの手順では、交差しない部分を削除します。
  2. ターゲットの直接の親ブロックから始まり、外側に向かって移動し、それぞれの包含ブロックのクリッピングが (存在すれば) 交差する長方形に適用されます。ブロックのクリッピングは、2つのブロックの交差と、 overflow プロパティで (存在すれば) 指定されたクリッピングモードに基づいて決定されます。 overflowvisible 以外を設定すると、クリッピングが行われます。
  3. 包含する要素の1つがネストされた閲覧コンテキストのルートである場合 (<iframe> に含まれる文書など)、交差する矩形は含まれているコンテキストのビューポートで切り取られ、コンテナー群を通して上方に再帰的にコンテナーの包含ブロックを続けます。ですから、最上位の <iframe> に到達したら、交差矩形はフレームのビューポートに切り取られ、フレームの親要素が次のブロックとなり、交差ルートに向けて再帰が行われます。
  4. 上方への再帰が交差ルートに達すると、結果の矩形が交差ルートの座標空間に対応付けられます。
  5. 結果の矩形はそれからルート交差矩形と交差することで更新されます。
  6. この矩形は、最終的に、ターゲットの document の座標空間に対応付けられます。

交差状態の変化のコールバック

ターゲット要素がルート要素内で見えている範囲が可視量の閾値を通過したとき、 IntersectionObserver オブジェクトのコールバックが実行されます。コールバックは、入力引数として交差したすべての閾値を示す IntersectionObserverEntry オブジェクトの配列を、また参照として IntersectionObserver オブジェクト自身を受け取ります。

閾値のリスト内のそれぞれの項目は、通過した閾値を説明する IntersectionObserverEntry オブジェクトです。つまり、それぞれの項目は指定された要素がルート要素とどれだけ交差したのか、要素が交差したと言えるのかどうか、推移が発生した方向を示します。

以下のコードスニペットは、要素がルート要素と交差していない状態から少なくとも75%が交差した状態まで推移した回数を数え続けて表示します。

intersectionCallback(entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      let elem = entry.target;

      if (entry.intersectionRatio >= 0.75) {
        intersectionCounter++;
      }
    }
  });
}

インターフェイス

IntersectionObserver
Intersection Observer API のプライマリーなインターフェイスです。同一の交差設定に対して任意の数のターゲット要素を監視するオブザーバーを作成し管理するためのメソッドを提供します。各オブザーバーは1つ以上のターゲット要素と共通の親要素、もしくは最上位のDocumentviewport との交差における変化を非同期的に監視することが出来ます。この親要素もしくはビューポートはルートと呼ばれます。
IntersectionObserverEntry
スクロールにおける変化の特定の瞬間において、ターゲット要素とルートとなる入れ物との交差を表現します。このタイプのオブジェクトは2つの方法でのみ得られます: IntersectionObserver コールバックへの入力として、または IntersectionObserver.takeRecords() を呼び出すことによって、の2つです。

単純な例

この単純な例では、ターゲット要素の色と透明度を要素の可視性で変化させます。Intersection Observer API を利用した時間の絡んだ要素の可視性では、要素 (例えば広告など) セットがユーザーに表示される時間を測定し、統計を記録したり要素を更新したりしてその情報にユーザーどう反応したかを示す、より拡張性の高い具体例を見ることが出来るようでしょう。

HTML

この例における HTML は非常に短く、主な要素はターゲットとなるボックス (IDは "box" としました) とボックス内のコンテンツです。

<div id="box">
  <div class="vertical">
    Welcome to <strong>The Box!</strong>
  </div>
</div>

CSS

この例では CSS はあまり重要ではありません。要素をレイアウトしbackground-colorborder 属性を CSS トランジションに適用させます。CSS transitions は要素の変化に多少変化が起きることを確認するために使用します。

#box {
  background-color: rgba(40, 40, 190, 255);
  border: 4px solid rgb(20, 20, 120);
  transition: background-color 1s, border 1s;
  width: 350px;
  height: 350px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.vertical {
  color: white;
  font: 32px "Arial";
}

.extra {
  width: 350px;
  height: 350px;
  margin-top: 10px;
  border: 4px solid rgb(20, 20, 120);
  text-align: center;
  padding: 20px;
}

JavaScript

最後に、Intersection Observer API を使って何が出来るか、 JavaScript のコードを見ていきましょう。

セットアップ

まずは、いくつかの変数を準備してオブザーバーをインストールする必要があります。

const numSteps = 20.0;

let boxElement;
let prevRatio = 0.0;
let increasingColor = "rgba(40, 40, 190, ratio)";
let decreasingColor = "rgba(190, 40, 40, ratio)";

// Set things up
window.addEventListener("load", (event) => {
  boxElement = document.querySelector("#box");

  createObserver();
}, false);

セットアップした定数と変数は下記の通りです。

numSteps
視認率が0.0から1.0の間にどのくらいの数の閾値を設定するか示す定数です。
prevRatio
この変数は閾値を超えた最後の視認率を記録するために使用します。これはターゲット要素が大体見えるようになったかどうかを調べることが出来ます。
increasingColor
視認率が増加している時にターゲット要素に適用する色を定義する文字列です。文字列の中の "比率" という単語はターゲット要素の現在の視認率に置き換えられ、要素が色を変化させるだけでなく不透明になるにつれて透明度が増していきます。
decreasingColor
同様に、視認率が減少していく時に適用する色を定義する文字列です。

Window.addEventListener() を呼び出してload イベントのリスンを開始します。ページロードが完了すると、querySelector() を使用して ID "box" 要素への参照を取得し createObserver() メソッドを呼び出して Intersection Observer の設定・インストール処理を開始します。

Intersection Observer の作成

createObserver() メソッドは新しい IntersectionObserver を作成し、ターゲット要素の監視を開始するためにページが完全にロードされてから呼び出されます。

function createObserver() {
  let observer;

  let options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()
  };

  observer = new IntersectionObserver(handleIntersect, options);
  observer.observe(boxElement);
}

この関数ではオブザーバーの設定を含む options オブジェクトを設定することから始めます。ドキュメントビューポートに対してターゲット要素がどのくらい見えているかという変化を監視したいので、rootnull にします。マージンは必要がないので、マージンオフセットである rootMargin 設定は "0px" と指定しています。これによって、オブザーバーは追加された (もしくは差し引かれた) スペースがなくてもターゲット要素の境界とビューポートの境界の交差点がどう変化するのか監視を開始することが出来ます。

視認率の閾値のリストである、thresholdは関数 buildThresholdList() によって構成されます。閾値のリストは、この例ではプラグラムによって計算されています。その数が意図的に調整可能だからです。

options が用意できたら、新しいオブザーバーを作成、つまりIntersectionObserver() のコンストラクタを呼び出して、閾値をまたいだ際に呼ばれる関数 handleIntersect() を指定し、オプションを指定します。次に、返されたオブザーバーに対して observe() を呼び出し、必要なターゲット要素を渡します。

observer.observe() をそれぞれの要素に対して呼び出すことにより、ビューポートに対して交差し変化しているかを複数の要素から監視することが出来ます。

閾値比率の配列を組み立てる

閾値のリストを作成する buildThresholdList() 関数は次のようになります。

function buildThresholdList() {
  let thresholds = [];
  let numSteps = 20;

  for (let i=1.0; i<=numSteps; i++) {
    let ratio = i/numSteps;
    thresholds.push(ratio);
  }

  thresholds.push(0);
  return thresholds;
}

これは 1 と numSteps の間の各整数 i に対して、値 i/numSteps を閾値の配列に入れることで、それぞれが 0.0 と 1.0 の間の比率である閾値の配列を作成しています。また、0 を配列に含めます。デフォルトの numSteps (20) が指定された結果、以下の閾値のリストが表示れます。

# Ratio # Ratio
1 0.05 11 0.55
2 0.1 12 0.6
3 0.15 13 0.65
4 0.2 14 0.7
5 0.25 15 0.75
6 0.3 16 0.8
7 0.35 17 0.85
8 0.4 18 0.9
9 0.45 19 0.95
10 0.5 20 1.0

もちろん、閾値の配列をハードコードすることは可能ですし、よくやりがちなことです。しかし、この例では設定を追加することで粒度を調整する余地が残っています。

交差の変換の処理

ブラウザーはターゲット要素 (このケースでは "box" というIDを持つ要素です) が表示されているか、またはどのくらい見えているかという比率が、閾値のリストにある値の1つをまたぐことを検出して、handleIntersect() が呼び出されます:

function handleIntersect(entries, observer) {
  entries.forEach((entry) => {
    if (entry.intersectionRatio > prevRatio) {
      entry.target.style.backgroundColor = increasingColor.replace("ratio", entry.intersectionRatio);
    } else {
      entry.target.style.backgroundColor = decreasingColor.replace("ratio", entry.intersectionRatio);
    }

    prevRatio = entry.intersectionRatio;
  });
}

リストである entries 内にある IntersectionObserverEntry について、entry の intersectionRatio が上昇しているかを調べます。上昇していればターゲットの background-colorincreasingColor ("rgba(40, 40, 190, ratio)" だったことを思い出してください) の値をセットし、その際にその中にある "ratio" という文字列を entry が持つ intersectionRatio と置き換えます。その結果、色が変更されるだけでなく、ターゲット要素の透明度も変更されます。交差する比率が下がるに連れて、背景色のアルファ値が下がりより透明度の高い要素となります。

同様に、 intersectionRatio が下がっている場合は decreasingColor を文字列として使用し "ratio" という文字列を intersectionRatio でもって置き換えたあとに、要素の background-color として適用します。

最後に、交差する割合が上がっているか下がっているかを追跡するために、変数 prevRatio に現在の比率を代入しておきます。

結果

以下がその結果内容です。ページを上下にスクロールして、ボックスの外観がどう変化するかを確認してみましょう。

より応用的な例はTiming element visibility with the Intersection Observer API のセクションを見てください。

仕様書

仕様書 状態 備考
Intersection Observer 草案

ブラウザーの対応

BCD tables only load in the browser

関連情報