📡

スクロール量を検知し、イベントを発火させる

2024/04/21に公開

はじめに

最近同意画面などでよく見かける最下部までスクロールしたらボタンが活性化するアレを実装する機会がありまして、どうしたら作れるか調べてみた。
そしたら observer でスクロールを検知してイベントを発火させるやり方があったので、observer とは何か DEMO を交えてまとめていきます。

デモ

デモはこちら
※赤い線を最下部に置くと同意画面でよくみるアレができます!

obserber とは?

①html に到達したらイベントを発火させる要素を配置する

<!--  基準となる要素  -->
<div id="bottomElement"></div>

② 後は下記のようにイベントを設定する

// 基準となる要素を取得する(赤い線の要素)
const bottomElement = document.getElementById('bottomElement');

const observerCallback = (entries, observer) => {
  // 発火させたいイベント
  const [entry] = entries;
  // 基準となる要素を跨いだか判定する
  if (entry.isIntersecting) {
    // 実行したい処理
  }
};

const observer = new IntersectionObserver(observerCallback, {
  root: null, // ビューポートをルートとする
  threshold: 0, // 要素が完全にビューポートに入った時にcallbackを呼び出す
});

observer.observe(bottomElement);

// ページがアンロードされる時にObserverを解除
window.addEventListener('unload', () => {
  observer.disconnect();
});

※補足
IntersectionObserver の第二引数 は下記のように設定できる。
以下公式ドキュメント引用

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

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

おまけの Vue ver

Vue サンプル
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';

const bottomElement: Ref<HTMLElement | null> = ref(null);
const isButtonActive = ref(false);

const observerCallback = (
  entries: IntersectionObserverEntry[],
  observer: IntersectionObserver
) => {
  const [entry] = entries;
  isButtonActive.value = entry.isIntersecting;
};

let observer: IntersectionObserver | null = null;

onMounted(() => {
  if (bottomElement.value) {
    observer = new IntersectionObserver(observerCallback, {
      root: null,
      threshold: 0,
    });
    observer.observe(bottomElement.value);
  }
});

onUnmounted(() => {
  observer?.disconnect();
});

const buttonClickHandler = (): void => {
  alert('ボタンが活性化され、クリックされました!');
};
</script>

<template>
  <div class="relative">
    <div class="content">
      <!-- 要素の内容 -->
      content area
    </div>

    <!-- 親要素の最下部を検出するための要素 -->
    <div ref="bottomElement" class="h-1"></div>

    <!-- 画面最下部に固定されたボタン -->
    <button
      :class="{ 'bg-blue-500': isButtonActive, 'bg-gray-300': !isButtonActive }"
      class="fixed bottom-0 left-0 w-full p-4 text-white"
      :disabled="!isButtonActive"
      @click="buttonClickHandler"
    >
      最下部に達したら活性化
    </button>
  </div>
</template>

<style>
.content {
  min-height: 200vh;
  background: wheat;
}
</style>

終わりに

特定の位置までスクロールしたらアニメーション発火させるなど、これからも使う機会はありそうだな〜

Discussion