要素がトップにきたことを感知する-IntersectionObserver

要素がトップにきたことを感知する-IntersectionObserver

ウェブページで、ある要素があるところへきたらアニメーションが始まるとか、コンテンツを下まで表示したら次のコンテンツをその下に表示する(無限スクロール)といったエフェクトをよく見かけますが、あれは Javascript の IntersectionObserver というインターフェイスを使って実現しているものです(調べていないので多分…)。

その IntersectionObserver を使って、ある要素がビューポートのトップにきたらあることを実行するという UI を作ろうと思います。

01どういうものかのサンプル

現在、当サイトはリニューアル準備中です。その新しいテーマのホームページ(フロントページ)は、メインビジュアル(キービジュアル)と新着記事、PickUp、よく読まれている記事の3つのセクションで構成する予定であり、その際に各セクションをワンクリックで移動するボタンをつくろうと思います。

下がそのメインビジュアルで、「これ」とあるボタンをクリックしますと順番にセクション1、2、3と移動していくイメージです。

実際にはこのエフェクトはすでに完成しており、デモサイトがあります。

  • デモサイト(現在停止中)

やろうとしていることは、次の画像のイメージです。

  • ホームページにアクセスがあればビューポートいっぱいにメインビジュアルを表示する
  • ボトムの ▼ がクリックされると section1 がビューポートトップにくるまでスクロールする
    同時に、 ▼ のリンク先を section2 に書き換える
  • section1 がトップにある状態、あるいはスクロールされているが、まだ section2 がトップに来ていない状態で ▼ がクリックされると section2 がトップにくるまでスクロールする
  • section2 でも同様のことを繰り返す
  • section3 がトップに来た場合は、次はないので ▼ を非表示にする
  • 逆にスクロールされて各セクションがトップから下に離れた場合は、 ▼ のリンク先を該当セクションに戻す
    たとえば、section1 がトップに来たときにはリンク先が section2 になっているが、逆スクロールされてトップから下に離れた場合にはリンク先を section1 に戻す

結構面倒なことに感じますがやってみましょう(笑)。

02IntersectionObserver

IntersectionObserver とは「交差オブザーバー」と訳されますが、監視領域に、監視対象の要素が入ったら教えてくれるというもので、多くの場合、監視領域はビューポート(ブラウザの表示領域)であり、監視対象の要素とは今回の場合は section1、2、3 です。また、監視領域に入ったかどうかの判断はどこまで入ったか、つまりビューポートの何%まで入ったか(見えるようになったか)を指定することができます。

なお、この IntersectonObserver は非同期で監視しますのでスクロールイベントのような負荷はかかりません。

IntersectionObserber の生成

まず、オプション(次項)を指定して IntersectionObserver のインスタンスを生成します。callback はコールバック関数の指定です。関数名はなんでもいいです。

const options = {
  root: null, // ビューポート
  rootMargin: '0px', // rootのマージン(top,right,bottom,left)
  threshold: 0 // しきい値
};

const observer = new IntersectionObserver(callback, options);

オプション

root は、監視する領域で、初期値は null でビューポートです。他の場合は getElementbyId() などを使い要素名で指定します。

rootMargin は、監視領域の範囲をマージン値を使って広くしたり狭くしたりできます。たとえば、ビューポートの上半分を監視領域にする場合は、'0% 0% -50% 0%' と指定します。単位は px または % だけです。その2つが混在することは問題ありませんが、単位をつけなかったり他の単位を指定しますとエラーになります。この値は後述する IntersectionObserverEntry のプロバティ rootBounds を見ることでわかります。

threshold は、監視対象の要素、今回の場合は各セクションの何割が監視領域に入った場合に知らせるかを決める数値です。0 から 1.0 の数値で指定します。0 の場合は 1px でも監視領域に入れば知らせてくれますし、1 の場合は要素が完全に監視領域に入れば知らせてくれます。[0, 0.5, 1] など複数指定できます。

監視する要素を指定する

次に監視対象の要素を指定します。これは単一でもいいですし、複数も指定できます。複数の場合は要素に同じクラス名をつけておきます。今回の場合は、3つのセクションに front-page-articles のクラス名をつけてありますので次のようになります。

// 単一の場合
const target = document.querySelector('セレクター');
observer.observe(target);

// 複数の場合
const targets = document.querySelectorAll('.front-page-articles');
targets.forEach( (target) => {
	observer.observe(target);
});

これで各セクションが監視領域に入った時にコールバック関数を呼び出してくれます。

03要素がトップにきたら…

以上が IntersectionObserver の基本です。

で、今回の場合は各セクションがトップに来たらですのでそのオプション設定を考えます。まず、'0% 0% -100% 0%' でどうかと思いつきますが、このときの監視領域がどうなっているの見てみます。

次のコールバック関数を書き、エントリー(監視対象の要素)の entry.rootBounds を見てみます。

// コールバック関数
const callback = (entries, observer) => {
	entries.forEach((entry) => {
	//	console.log(entry.boundingClientRect);
	//	console.log(entry.intersectionRatio);
	//	console.log(entry.intersectionRect);
	//	console.log(entry.isIntersecting);
		console.log(entry.rootBounds);
	//	entry.time
	});
};

まず、'0% 0% 0% 0%' の場合です。

DOMRectReadOnly {x: 0, y: 0, width: 1023, height: 924, top: 0, …}
bottom: 924
height: 924
left: 0
right: 1023
top: 0
width: 1023
x: 0
y: 0

監視領域がビューポート(1023px, 924px)であることがわかります。

次に '0% 0% -100% 0%' の場合です。

DOMRectReadOnly {x: 0, y: 0, width: 1023, height: 0, top: 0, …}
bottom: 0
height: 0
left: 0
right: 1023
top: 0
width: 1023
x: 0
y: 0

これですと監視領域の height が 0 になってしまいうまくいきません。いろいろやってみところでは、'-10% 0% -90% 0%' では height:1 となったりするのですが、'-50% 0% -50% 0%' では height:0 となります。おそらく %値ですので px値にする際に整数に丸められるためだと思います。px値で指定するのも面倒ですので、結局、'-1% 0% -99% 0%' とすることにします。

オプション値

監視対象の要素がビューポートのトップにきたことを感知するオプション値は次の通りです。

const options = {
  root: null, // ビューポート
  rootMargin: '-1% 0% -99% 0%', // rootのマージン(top,right,bottom,left)
  threshold: 0 // しきい値
};

監視領域から対象が外れたら…

次に、今回のケースでは監視対象の要素である section1、2、3 が監視領域から外れた場合を感知する必要があります。これは IntersectionObserver の仕様として、領域に入った場合と領域から完全に外れた場合を感知するようになっていますので簡単です。

プロバティの entry.isIntersecting を使います。これは要素が領域に入った場合に true を返し、外れた場合に false を返します。

04コールバック関数

ということで完成したコードは次のようになります。

// コールバック関数
const callback = (entries, observer) => {
	entries.forEach((entry) => {
		let next = Number(entry.target.id.replace(/[^0-9]/g, ''));
		if(entry.isIntersecting){
			next += 1;
			if(null === document.getElementById('section' + next)){
				document.querySelector('.section-link-wrap').style.display = 'none';
			}else{
				document.querySelector('.section-link-wrap').style.display = 'block';
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}else{
			if(0 === next){
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}
	});
};
 
// Intersection Observerのインスタンス生成
const options = {
	root: null,
	rootMargin: '-1% 0% -99% 0%',
	threshold: 0
};
const observer = new IntersectionObserver(callback, options);

// 監視対象の要素
const targets = document.querySelectorAll('.front-page-articles');
targets.forEach( (target) => {
	observer.observe(target);
});

なお、これまで監視対象の要素であるセクションは section1、2、3 と表記していますが、上のコードの要素は加減算の都合で、

<section id="section0" class="front-page-articles">
</section>
<section id="section1" class="front-page-articles">
</section>
<section id="section2" class="front-page-articles">
</section>

と、0 から始まっています。

05デモサイト

  • デモサイト (現在停止中)

以上です。