今までスクロールに応じて要素をフェードインやボックススライドイン表示等のエフェクトを付与していましたが、 Intersection Observer API を知ったので試してみることにします。
経緯
きっかけは BackstopJS の試験 (BackstopJS で背景画像の高さを `vh` 単位で指定したページで画像やスクリーンショットが引き伸ばされる現象についてメモ (未解決)) の調査の中で、「スクロールイベントによる要素の表示は上手く検知できない」的な記事を見かけたことです(どの記事だったかは失念)。
その中で、「代わりに Intersection Observer API を使うと上手く行く」という記載で存在を知りました。
コード
ということでまずは成果物を。
リポジトリ。
HTML
<!-- ヘッダに対する要素 -->
<section class="container pb-5 graharaja">
<!-- 略 -->
</section>
<!-- /ヘッダに対する要素 -->
<!-- フェードイン表示させる要素群 -->
<div class="container pb-5 mahabharata">
<h2 class="c-heading_maha mt-4 mb-3">Mahabharata</h2>
<section class="c-parva p-3 mb-4">
<h3 class="c-heading_chota mt-4 mb-3">Adi Parva</h3>
<!-- 略 -->
</section>
<section class="c-parva p-3 mb-4">
<h3 class="c-heading_chota mt-4 mb-3">Sabha Parva</h3>
<!-- 略 -->
</section>
<!-- 略 -->
</div>
<!-- /フェードイン表示させる要素群 -->
HTMLは適当に配置します。今回試験するのは以下の2つです。
- ヘッダに背景色を付与:
.graharaja
(キービジュアル相当) の下端がブラウザの表示領域の上(外)に到達したらヘッダに背景色を付与- 逆に上スクロールで戻ってきて
.graharaja
要素がブラウザ表示領域内になったらヘッダの背景色を透明に戻す
- コンテンツをフェードイン表示
.mahabharata
(メインコンテンツ相当) 内の.c-parva
(各章) がブラウザ表示領域に入ったらフェードインで表示
css (Scss)
_l-hdeader.scss
@charset "utf-8";
@use "sass:color";
@use "../foundation" as f;
.l-header {
.navbar-brand {
&,
&:link,
&:visited {
color: f.$main-color;
}
&:hover,
&:active,
&:focus {
color: f.$main-color_l;
}
}
.navbar {
transition: background-color 0.3s ease;
&.active {
background-color: f.$main-color;
color: f.$color;
.navbar-brand {
&,
&:link,
&:visited {
color: f.$color;
}
&:hover,
&:active,
&:focus {
color: color.adjust(f.$color, $lightness: 10%);
}
}
}
}
}
最初は .navbar
の背景色は透明ですが、 .active
クラス が付与されると背景色が付きます。ついでに文字色も変更。
_c-parva.scss
@charset "utf-8";
@use "../../foundation" as f;
.c-parva {
box-sizing: border-box;
transition: opacity 1.6s, transform 1s;
opacity: 0;
transform: translateY(2rem);
&.active {
opacity: 1;
transform: translateY(0);
}
}
最初 opacity: 0;
, transform: translateY(2rem);
で透明かつ下にずらしていた 各.c-parva
を、.active
クラスを付与することで表示・元の位置に引き上げるようにします。
JavaScript
/**
* ヘッダの表示
*/
const headerShow = () => {
// クラス付与要素
const headers = document.querySelectorAll('.l-header .navbar');
// 監視対象要素
const graharajas = document.querySelectorAll('.graharaja');
// DOM to Array
const graharajasArray = Array.prototype.slice.call(graharajas, 0);
// options
const options = {
root: null,
rootMargin: '0px',
threshold: 0
};
/**
* callback
*
* @param elms
*/
const santaCrossHeader = (elms) => {
const elmsArray = Array.prototype.slice.call(elms, 0);
for (const elm of elmsArray) {
// ブラウザ表示領域に対する対象要素の位置
const elmRectCoor = elm.target.getBoundingClientRect();
if ( 0 > elmRectCoor.bottom ) {
// ブラウザ表示領域に対する対象要素の上端の位置 が ブラウザの表示領域 より上
headers[0].classList.add('active');
}
else {
headers[0].classList.remove('active');
}
}
};
// instance
const observer = new IntersectionObserver(santaCrossHeader, options);
// observe
for (const graharaja of graharajasArray) {
observer.observe(graharaja);
}
return observer;
};
/**
* 章ごとのフェードイン表示
*
* @param clientHeight {Number} : ブラウザの高さ
*/
const parvasShow = (clientHeight) => {
// 監視対象要素
const parvas = document.querySelectorAll('.c-parva');
// DOM to Array
const parvasArray = Array.prototype.slice.call(parvas, 0);
// options
const options = {
root: null,
rootMargin: '0px 0px -12%',
threshold: 0
};
/**
* callback
*
* @param elms
*/
const santaCross = (elms) => {
const elmsArray = Array.prototype.slice.call(elms, 0);
for (const elm of elmsArray) {
// ブラウザ表示領域に対する対象要素の位置
const elmRectCoor = elm.target.getBoundingClientRect();
// if (elm.isIntersecting) { // この方法だと「交差しているか」なので対象要素が ブラウザ表示領域の上の場合反応しない(上にスクロールした際に表示される)
if ( elmRectCoor.top < clientHeight ) {
// ブラウザ表示領域に対する対象要素の位置 が ブラウザの高さ 未満 // この方法ならばブラウザの上にある要素も既に表示された状態になる
elm.target.classList.add('active');
}
}
};
// instance
const observer = new IntersectionObserver(santaCross, options);
// observe
for (const parva of parvasArray) {
observer.observe(parva);
}
return observer;
};
window.addEventListener('load', () => {
// ブラウザの高さ
let clientHeight = document.documentElement.clientHeight;
// ヘッダの表示
let headerObserver = headerShow();
// 章の表示
let parvasObserver = parvasShow(clientHeight);
window.addEventListener('resize', () => {
// resize でブラウザの表示領域の高さが変動したら
clientHeight = document.documentElement.clientHeight;
// 一旦監視を止める
headerObserver.disconnect();
parvasObserver.disconnect();
// 再度監視
headerObserver = headerShow();
parvasObserver = parvasShow(clientHeight);
});
});
今回は jQuery を使わずに書いてみました。やっていることはコメントにある通りですが、
headerShow()
: ヘッダの表示if
文 の条件0 > elmRectCoor.bottom
で.graharaja
の下端がブラウザの表示領域より上に去ったら、という判定をして、ヘッダに.active
を付与していますelse
で戻ってきたらヘッダの.active
を除去します
parvasShow()
: 各章の表示IntersectionObserver
に渡すオプションで下方向-12%
を指定することで、「ブラウザの表示領域に入ったらすぐ」ではなく、もう少し中まで入ってからフェードイン表示が始まるように調整しています
これで意図した挙動になったので fixed 。
参考
Intersection Observer API
- JSでのスクロール連動エフェクトにはIntersection Observerが便利 – ICS MEDIA
- Intersection Observer が良さそうなので試してみた – Qiita
- Intersection Observer を用いた要素出現検出の最適化 | blog.jxck.io
- IntersectionObserverで要素が画面内に入ってきたかを判定する