【更新日 :

【CSS , JavaScript】Intersection Observerでスクロールアニメーションを実装する

Category: css / js

Intersection Observerでスクロールアニメーションを実装したサンプルです。
ブレイクポイント毎の設定変更、単一orエリアごとに発火(複数同時にもアニメーションできる)する機能があります。


See the Pen
Intersection Observer Scroll Animation
by Kazuma Sakata (@sakata-kazuma)
on CodePen.


HTML

■複数アニメーションする場合
・親要素に .js-anime-box を追加。
・アニメーションさせたい内包要素に .js-anime-elm を追加。

■単一アニメーションの場合
・要素に .js-anime-box と .js-anime-elm を追加。

<div class="js-anime-box">
  <p class="js-anime-elm">エリア内複数アニメーション要素1</p>
  <p class="js-anime-elm">エリア内複数アニメーション要素2</p>
</div>

<p class="js-anime-box js-anime-elm">単一アニメーション要素</p>

CSS

.js-anime-elm に .is-anime が追加されると発火します。
サイトに合わせて適宜調整をしてください。

.js-anime-elm {
  opacity: 0;
  animation-fill-mode: both;
  animation-duration: 1.2s;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

.is-anime {
  animation-name: fadeIn;
}
.is-anime.is-anime-fadeInUp {
  animation-name: fadeInUp;
}
@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
@keyframes fadeInUp {
  0% {
    opacity: 0;
    transform: translate3d(0,100px,0);
  }
  100% {
    opacity: 1;
    transform: translate3d(0,0,0);
  }
}

JS

//Intersection Observerスクロールアニメーション
function observerAnime(setOptions) {
  //options
  const defaultOptions = {
    root: null,  //IntersectionObserverのroot
    rootMargin: '0px',  //IntersectionObserverのrootMargin
    threshold: 0,  //IntersectionObserverのthreshold
    loop: false,  //アニメーション繰り返し設定
    responsive: null,  //レスポンシブ設定
    /*
      //レスポンシブ設定
      responsive: [{
        breakpoint: 1024, //ブレイクポイント max-width
        rootMargin: '0px',
        threshold: 0,
        loop: false
      }, {
        breakpoint: 500,
        rootMargin: '0px',
        threshold: 0,
        loop: false
      }]
    */
    targetAreaClass : '.js-anime-box', //ターゲットを内包するエリアclass
    targetClass: '.js-anime-elm', //ターゲットclass
    animeSetClass: 'is-anime',  //アニメーション実行時に追加するclass
  }
  //設定をマージ
  const options = Object.assign({}, defaultOptions, setOptions);

  //ターゲット要素取得
  let targets = document.querySelectorAll(options.targetAreaClass);
  //要素が存在するかチェック
  if(targets.length === 0) {
    return false;
  }

  //各設定を取得
  const targetClass = options.targetClass;
  const animeSetClass = options.animeSetClass;
  options.loopFlg = options.loop;


  //observer初期設定
  let observer;
  setObserver(options);

  //breakpoint切り替え監視
  let breakpoint = getBreakpoint(options);
  //breakpointが設定されていたら処理する
  if(breakpoint !== null) {
    let nowBreakpoint = breakpoint;
    window.addEventListener('resize', function() {
      //ブレイクポイントが切り替わったタイミンでobserverをリセット・再定義
      breakpoint = getBreakpoint(options);
      if(nowBreakpoint !== breakpoint) {
        nowBreakpoint = breakpoint;
        //observerリセット
        targets.forEach(function(target){
          observer.unobserve(target);
        });
        //observer再定義
        setObserver(options);
      }
    });
  }

  //observer定義関数
  function setObserver(options) {
    //ブレイクポイントに応じたオプションを取得
    const getOptions = getOptionsFunc(options);
    options.loopFlg = getOptions[1];
    //observer設定
    observer = new IntersectionObserver(setAnimeClass, getOptions[0]);
    targets.forEach(function(target){
      observer.observe(target);
    });
  }

  //アニメーション処理
  function setAnimeClass(entries, observer) {
    //ループフラグ取得
    const loopFlg = options.loopFlg;
    entries.forEach(entry => {
      //ターゲット要素取得
      const target = entry.target;
      //ターゲット要素の子要素取得
      const children = target.querySelectorAll(targetClass);

      //要素が画面に入ると処理
      if(entry.isIntersecting) {
        //子要素がある場合は子要素にclassを追加
        if(children.length !== 0) {
          children.forEach(function(child){
            child.classList.add(animeSetClass);
          });

        //子要素がない場合はターゲット自身にclassを追加
        } else {
          target.classList.add(animeSetClass);
        }

        //ループなしの場合
        if(!loopFlg) {
          //要素の監視を解除
          observer.unobserve(target);
        }

      //ループありの場合
      } else if(loopFlg) {
        //子要素がある場合は子要素に設定されたclassを削除
        if(children.length !== 0) {
          children.forEach(function(child){
            child.classList.remove(animeSetClass);
          });

        //子要素がない場合はターゲット自身のclassを削除
        } else {
          target.classList.remove(animeSetClass);
        }
      }
    });
  }

  //ブレイクポイントごとの設定取得関数
  function getBreakpoint(options) {
    let responsive = options.responsive;
    //ブレイクポイントなしの場合は処理しない
    if(!responsive) {
      return responsive;
    }

    //ウィンドウの幅取得
    const winWidth = window.innerWidth;
    //ウィンドウの幅とブレイクポイントを比較・取得
    let reverse = responsive.concat().reverse();
    let breakpoint = 'full';
    for (let i = 0; i < reverse.length; i++) {
      if(winWidth <= reverse[i].breakpoint) {
        breakpoint = reverse[i].breakpoint;
        break;
      }
    }
    return breakpoint;
  }

  //オプション取得関数
  function getOptionsFunc(options) {
    //ベース設定取得
    let root = options.root;
    let rootMargin = options.rootMargin;
    let threshold = options.threshold;
    let loop = options.loop;

    //レスポンシブ切り替え
    const responsive = options.responsive;
    //ブレイクポイントありの場合は設定を取得
    if(responsive) {
      //ウィンドウの幅取得
      const winWidth = window.innerWidth;
      //ウィンドウの幅とブレイクポイントを比較・各設定取得
      for (let i = 0; i < responsive.length; i++) {
        if(winWidth <= responsive[i].breakpoint) {
          root = responsive[i].root;
          rootMargin = responsive[i].rootMargin;
          threshold = responsive[i].threshold;
          loop = responsive[i].loop;
        }
      }
    }

    //IntersectionObserverのオプションとループフラグを返す
    return [
      {
        root: root,
        rootMargin: rootMargin,
        threshold: threshold
      },
      loop
    ];
  }
}

関数の呼び出し

任意の場所で以下を実行し関数を呼び出してください。
loop設定を有効化する場合、rootMarginは'0px'設定が良さそうです。

//Intersection Observerスクロールアニメーション関数呼び出し
observerAnime({
  rootMargin: '-50%', //画面の中央で発火。IntersectionObserverのrootMargin デフォルトは'0px'
  loop: false, //アニメーション繰り返し設定 デフォルトはfalse
  threshold: 0, //threshold デフォルトは0
  responsive: [{
    breakpoint: 1024, //ブレイクポイント max-width
    rootMargin: '-30%',
  }, {
    breakpoint: 400,
    rootMargin: '0px',
    loop: true, //trueにする場合、rootMarginは'0px'が良さそうです
  }]
});

オプション

root: null,  //IntersectionObserverのroot
rootMargin: '0px',  //IntersectionObserverのrootMargin
threshold: 0,  //IntersectionObserverのthreshold
loop: false,  //アニメーション繰り返し設定
responsive: null,  //レスポンシブ設定
/*
  //レスポンシブ設定
  responsive: [{
    breakpoint: 1024, //ブレイクポイント max-width
    rootMargin: '0px',
    threshold: 0,
    loop: false
  }, {
    breakpoint: 500,
    rootMargin: '0px',
    threshold: 0,
    loop: false
  }]
*/
targetAreaClass : '.js-anime-box', //ターゲットを内包するエリアclass
targetClass: '.js-anime-elm', //ターゲットclass
animeSetClass: 'is-anime',  //アニメーション実行時に追加するclass

ブレイクポイントなしのシンプル版

ブレイクポイント切り替えのないシンプル版も作ってみました。
レスポンシブ設定がない以外はすべて同じ設定です。


See the Pen
Intersection Observer Scroll Animation 【simple】
by Kazuma Sakata (@sakata-kazuma)
on CodePen.


JS

//【シンプル版】Intersection Observerスクロールアニメーション
function observerAnime() {
  //options
  const options = {
    root: null,  //IntersectionObserverのroot
    rootMargin: '-50%',  //IntersectionObserverのrootMargin
    threshold: 0,  //IntersectionObserverのthreshold
    loop: false,  //アニメーション繰り返し設定
    targetAreaClass : '.js-anime-box', //ターゲットを内包するエリアclass
    targetClass: '.js-anime-elm', //ターゲットclass
    animeSetClass: 'is-anime',  //アニメーション実行時に追加するclass
  }

  //ターゲット要素取得
  let targets = document.querySelectorAll(options.targetAreaClass);
  //要素が存在するかチェック
  if(targets.length === 0) {
    return false;
  }

  //各設定を取得
  const targetClass = options.targetClass;
  const animeSetClass = options.animeSetClass;
  options.loopFlg = options.loop;

  //observer初期設定
  let observer;
  setObserver(options);

  //observer定義関数
  function setObserver(options) {
    //オプションを取得
    options.loopFlg = options.loop;
    //observer設定
    observer = new IntersectionObserver(setAnimeClass, {
      root: options.root,
      rootMargin: options.rootMargin,
      threshold: options.threshold
    });
    targets.forEach(function(target){
      observer.observe(target);
    });
  }

  //アニメーション処理
  function setAnimeClass(entries, observer) {
    //ループフラグ取得
    const loopFlg = options.loopFlg;
    entries.forEach(entry => {
      //ターゲット要素取得
      const target = entry.target;
      //ターゲット要素の子要素取得
      const children = target.querySelectorAll(targetClass);

      //要素が画面に入ると処理
      if(entry.isIntersecting) {
        //子要素がある場合は子要素にclassを追加
        if(children.length !== 0) {
          children.forEach(function(child){
            child.classList.add(animeSetClass);
          });

        //子要素がない場合はターゲット自身にclassを追加
        } else {
          target.classList.add(animeSetClass);
        }

        //ループなしの場合
        if(!loopFlg) {
          //要素の監視を解除
          observer.unobserve(target);
        }

      //ループありの場合
      } else if(loopFlg) {
        //子要素がある場合は子要素に設定されたclassを削除
        if(children.length !== 0) {
          children.forEach(function(child){
            child.classList.remove(animeSetClass);
          });

        //子要素がない場合はターゲット自身のclassを削除
        } else {
          target.classList.remove(animeSetClass);
        }
      }
    });
  }
}

関数の呼び出し

任意の場所で以下を実行し関数を呼び出してください。

//関数呼び出し
observerAnime();

参考リンク

Category : css / js