Intersection Observer API を試す

今までスクロールに応じて要素をフェードインやボックススライドイン表示等のエフェクトを付与していましたが、 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つです。

  1. ヘッダに背景色を付与:
    • .graharaja (キービジュアル相当) の下端がブラウザの表示領域の上(外)に到達したらヘッダに背景色を付与
    • 逆に上スクロールで戻ってきて .graharaja 要素がブラウザ表示領域内になったらヘッダの背景色を透明に戻す
  2. コンテンツをフェードイン表示
    • .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

Array.prototype.slice.call(arguments)

getBoundingClientRect

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。