Intersection observer によるスクロールアニメーション

Akira

福岡在住ウェブデザイナー。Web サイト制作に役立つ情報を紹介しています。AMP が大好き。

FacebookTwitter で記事の更新をお知らせしています。

多くのサイトでよく目にするのが、画面内に入った要素が動くアニメーション。

下にスクロールしていくと「画像などがふんわりと表示」「左や右から出てくる」「小さかったものが大きくなる」など、サイトを彩るアニメーションを見かけます。

このようなアニメーションは、以前は下記のようなスクロールイベントで実現していました。

// jQuery の場合
$(window).scroll(function() {
処理を書く
});

ただ、スクロールイベントは、ブラウザに負荷をかけてしまいます。というのも、スクロールをする度に、処理がなされてしまうためです。何も対策をしなければ、「重い」サイトになる弊害がありました。また、その対策には限度がありました。

そこで最近使われているのが、Intersection observer 。

ブラウザに負荷をかけずに、スクロールで画面内に入った要素にアニメーションを指定できます。

Intersection observer とは?

Intersection observer は、交点を監視し、要素が交差した時にイベントを発生させる API です。

詳しくは、「Intersection Observer API」「Intersection Observer を用いた要素出現検出の最適化」をご参考ください。

簡単に言うと「スクロールイベントよりパフォーマンスが上がる」「開発の手間が省ける」もの。

今回ご紹介するアニメーションのほかに、画像の遅延読み込みにも利用できます。Google は、Intersection observer を使った画像の遅延読み込みを推奨しています(ネイティブ lazy-load loading="lazy" のフォールバック用として使用を推奨)。

執筆時点で Intersection observer に対応しているブラウザは、Chrome・Firefox・Edge です(iOS 版はいずれも未対応)。

Can I use… : IntersectionObserver

ただ、対応していないブラウザのために、Polyfill が W3C より提供されています。

実例

Intersection observer を使用し、画面内に入った要素が動くアニメーションの実例を紹介します。

早速 3 枚の画像によるサンプルをご覧ください。

HTML は、単純なものです。

<div class="sample sample-one"><img 以下省略></div>
<div class="sample sample-second"><img 以下省略></div>
<div class="sample sample-third"><img 以下省略></div>

肝心の Intersection observer を使った JavaScript は、下記のとおりです。画像が画面内に入ると、<div> タグの class に sample-animation を追加します。また、画像が画面から出ると sample-animation を削除します。

(() => {
  const sample = document.querySelectorAll('.sample');
  
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.intersectionRatio > 0) {
        entry.target.classList.add('sample-animation');
      } else {
        entry.target.classList.remove('sample-animation');
      }
    });
  });
  
  sample.forEach(img => {
    observer.observe(img);
  });
})();

そして、最後に CSS はこちら。JavaScript で追加する sample-animation を使い、アニメーションを指定します。

/* 3 つのサンプル共通 */
.sample {
  overflow: hidden;
  position: relative;
}

.sample img {
  display: block;
  height: auto;
  width: 100%;
}

/* 1 つ目のサンプル */
.sample-one.sample-animation img {
  animation: sample-one 1.2s cubic-bezier(.4, 0, .2, 1);
}

@keyframes sample-one {
  0% {
    opacity: 0;
    transform: scale(1.2) translateY(24px);
  }
  
  32% {
    opacity: 0;
    transform: scale(1.2) translateY(24px);
  }
}

/* 2 つ目のサンプル */
.sample-second.sample-animation {
  animation: sample-second-img 2s cubic-bezier(.4, 0, .2, 1);
}

.sample-second.sample-animation:before {
  animation: sample-second-before 2s cubic-bezier(.4, 0, .2, 1) forwards;
  background: #fff;
  bottom: 0;
  content: '';
  left: 0;
  pointer-events: none;
  position: absolute;
  right: 0;
  top: 0;
  z-index: 1;
}

@keyframes sample-second-img {
  0% {
    opacity: 0;
  }
}

@keyframes sample-second-before {
  100% {
    transform: translateX(100%);
  }
}

/* 3 つ目のサンプル */
.sample-third.sample-animation:before,
.sample-third.sample-animation:after {
  animation: 2s cubic-bezier(.4, 0, .2, 1) forwards;
  background: #fff;
  bottom: 0;
  content: '';
  pointer-events: none;
  position: absolute;
  top: 0;
  z-index: 1;
}

.sample-third.sample-animation:before {
  animation-name: sample-third-before;
  left: 0;
  right: 50%;
}

.sample-third.sample-animation:after {
  animation-name: sample-third-after;
  left: 50%;
  right: 0;
}

@keyframes sample-third-before {
  100% {
    transform: translateY(100%);
  }
}

@keyframes sample-third-after {
  100% {
    transform: translateY(-100%);
  }
}

最初の 1 回のみアニメーション

先程の例では、画像が画面内に入る度にアニメーションが発動します。

画像が画面内に入った最初の 1 回目のみアニメーションを動かす場合は、JavaScript を下記のコードに変更します。

(() => {
  const sample = document.querySelectorAll('.sample');
  
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.intersectionRatio > 0) {
        entry.target.classList.add('sample-animation');
        // ターゲット要素の監視を停止
        observer.unobserve(entry.target);
      }
    });
  });
  
  sample.forEach(img => {
    observer.observe(img);
  });
})();

9 行目に IntersectionObserver.unobserve() を指定しています。

class にsample-animation を追加後は、要素の監視を停止します。

発動タイミングを遅らせる

これまでの 2 つの JavaScript は、いずれも要素が画面内に入ると、すぐにアニメーションが発動します。

アニメーションによっては、またはアニメーションを指定する対象によっては、アニメーションの発動を遅らせたい場合があります。

Intersection observer のオプション rootMargin を指定すれば、アニメーション発動のタイミングをコントロールできます。

rootMargin を指定したサンプルをご覧ください。下の 3 つの円が画面内に入ってから、さらに 200px スクロールした地点でアニメーションが発動します。

3 つの円の HTML は、下記のとおりです。

<ul class="sample-circles">
  <li class="sample-circle sample-circle-one"></li>
  <li class="sample-circle sample-circle-second"></li>
  <li class="sample-circle sample-circle-third"></li>
</ul>

JavaScript 。13 行目に rootMargin を指定しています。

(() => {
  const sample = document.querySelectorAll('.sample-circle');
  
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.intersectionRatio > 0) {
        entry.target.classList.add('sample-circle-animation');
      } else {
        entry.target.classList.remove('sample-circle-animation');
      }
    });
  }, {
    rootMargin: '-200px 0px'
  });
  
  sample.forEach(img => {
    observer.observe(img);
  });
})();

CSS 。

.sample-circles {
  display: flex;
  justify-content: space-around;
  list-style: none;
  padding: 0;
}

.sample-circle {
  background: #ff4081;
  border-radius: 50px;
  height: 80px;
  width: 80px;
}

.sample-circle.sample-circle-animation {
  animation: sample-circle-animation cubic-bezier(.4, 0, .2, 1);
}

.sample-circle-one.sample-circle-animation {
  animation-duration: .3s;
}

.sample-circle-second.sample-circle-animation {
  animation-duration: .4s;
}

.sample-circle-third.sample-circle-animation {
  animation-duration: .5s;
}

@keyframes sample-circle-animation {
  50% {
    transform: scale(1.2) translateY(-32px);
  }
}

rootMargin の指定方法は、CSS の margin と同じです(ただし、0 であっても px か % の単位が必要)。上下に負の値を指定すれば、アニメーションの発動タイミングが遅れます。

逆に、正の値の 200px を指定すると、要素の 200px 手前でイベントが発火します。この正の値の rootMargin を指定するのが、画像の遅延読み込みです。

画像は遅延で読み込みながらも、画面内に入る前に画像を表示でき、画像がなかなか表示されない症状を防げます。

画像の遅延読み込みは、「Lazy load images using IntersectionObserver」が参考になります。

また、「Lozad.js」は、評価の高い Intersection observer を使った遅延読み込み用ライブラリです。

コメント

  1. Akiraさん、おはようございます。

    質問です。
    もう一つ写真のアニメ記事がありますが、どちらがサイトに負担をかけない方法でしょうか?

    質問2
    JavaScriptの記述する場所は、どこが一番適切でしょうか?

    • こんにちは。

      ご質問 1

      もう 1 つの記事は、「画像を徐々に表示する CSS アニメーション」の最後の「スクロールアニメーション」でしょうか。

      以前は、他のサイトから引っ張ってきた jQuery を使っていました。ただ、あまり良くなかったので、現在はこの記事に書いている Intersection observer に変更しています。

      サイトに負担をかけないのは、 Intersection observer です。 Intersection observer が使えるものであれば、 Intersection observer の使用が理想です。

      ご質問 2

      JavaScript の読み込み方法や内容によって違うため、一概には言えないです。

      ただ、 <link rel="preload"> での事前読み込みは、通常 <head> 内に記述します。

      asyncdefer での非同期読み込み、インラインでの読み込みであれば、レンダリングをブロックしないので <head> 内でも </body> 直前でもどちらでもいいです。サイトに合わせて選択します。

      何も工夫せず普通に読み込む場合には、 </body> 直前がいいと思います。

      Cocoon をお使いのサイトであれば、 Cocoon の高速化設定の「 JavaScript を縮小化する」を有効にすると、特に記述場所を考える必要はありません。

  2. 回答ありがとうございます。

    JavaScriptですが、cocoonの親テーマのheader.phpの直前に記述すると、サイトヘッダー部分に記述した文字がでます。

    そこで、cocoonのカスタムcssとカスタムjavaScriptに書き込みました。

    自分のパソコンではokです。レスポシブテストでもokです。

    ただ、自分のiphoneでは画像が動いてくれません。
    現在、キャラバン隊の見出しの部分の写真6枚にアニメをしています。
    https://chan-bike.com/annecy

    iPhoneの Chrome ・ Firefoxでは動作しないのでしょうか?

    • Cocoon の場合には、基本的に JavaScript は子テーマの javascript.js に追加します。

      そのページでしか使わない場合には、カスタム JavaScript でも OK です。

      キャラバン隊の 6 枚の画像のうち、最初の 4 枚は意図しないアニメーションになっているように思えます。

      おそらく理由は、 Cocoon の高速化設定の「 CSS を縮小化する」と CSS の @keyframes の相性が悪いことです。

      例えば下記のような CSS を指定されているのなら…。

      @keyframes sample-one {
        0% {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
        32% {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
      }

      0% を from に変更する必要があります。

      @keyframes sample-one {
        from {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
        32% {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
      }

      これでもアニメーションが動かない場合には、ブラウザのキャッシュが怪しいです。

  3. こんにちは。確認作業続けてます。

    アンドロイドで確認すると、ちゃんと動いていました。

    私のiPhone seは動かないです。キャッシュもクリアーしてみたのですけど。

    windows xpのパソコンがあったので、確認したのですが、こちらは動きませんでした。

    • トップページのスクロールアニメーションは、 iOS でも動いているでしょうか。

      動いているのなら、コードの書き方に iOS が対応していないことが理由かもしれません。

      JavaScript 内の下記の部分を探し…

      sample.forEach( img => {
          observer.observe( img );
      });

      このように書き換えると、動くでしょうか。

      Array.prototype.forEach.call( sample, function( img ) {
          observer.observe( img );
      });

      IE でも動くように「 Cocoon の一覧リストに心地よいスクロールアニメーション」では、この書き方をしているんです。 iOS に対しても、この書き方でないといけないのかもしれません。

  4. 現在のcocoon 子テーマのJavaScript は以下です。

    iosでは、FirstLayoutもスマホでは動いてないです。

    //ここに追加したいJavaScript、jQueryを記入してください。
    //このJavaScriptファイルは、親テーマのJavaScriptファイルのあとに呼び出されます。
    //JavaScriptやjQueryで親テーマのjavascript.jsに加えて関数を記入したい時に使用します。
    ( function() {
        const list = document.querySelectorAll( '.entry-card-wrap' );
        
        const observer = new IntersectionObserver( function( entries ) {
            entries.forEach( function( entry ) {
                if( entry.intersectionRatio > 0 ) {
                    entry.target.classList.add( 'list-animation' );
                } else {
                    entry.target.classList.remove( 'list-animation' );
                }
            });
        }, {
            rootMargin: '40px 0px',
        });
        
        Array.prototype.forEach.call( list, function( card ) {
            observer.observe( card );
        });
    })();
    
    
    ( () => {
        // p タグの class を指定
        const sample = document.querySelectorAll( '.sample' );
        
        const observer = new IntersectionObserver( entries => {
            entries.forEach( entry => {
                if( entry.intersectionRatio > 0 ) {
                    entry.target.classList.add( 'sample-animation' );
                } else {
                    entry.target.classList.remove( 'sample-animation' );
                }
            });
        });
        
        Array.prototype.forEach.call( sample, function( img ) {
            observer.observe( img );
        });
    })();
    • 「これが原因かな?」と思ったものがあるので、問題を確定させるために 2 つ質問を致します。

      質問 1

      chan さんのサイトのトップページは、 iPhone で閲覧した際にスクロールアニメーションが動いているでしょうか。

      質問 2

      この記事の 3 枚の画像サンプルのコードを変更しました。この 3 枚の画像のアニメーションは、 chan さんの iPhone で動くでしょうか。ブラウザは Chrome でも Firefox でも Safari でも何でも構いません。

      お手数をお掛けし大変申し訳ありませんが、ご確認いただけないでしょうか。

  5. 現在、CSSをこちらの記事内の3つのサンプル共通に変えました。

    質問1
    書き変えた後には、記事一覧のリストに心地よいスクロールアニメーションがスマホで動かなくなりました。先走ってしまいました。正解をまた教えて頂けると助かります。

    質問2
    ブラウザにやさしくスクロールで画面に入った要素にアニメーションは、iPhoneで動いています。Chrome ・ Firefox ・Safari すぺてokです。

    /* 3 つのサンプル共通 */
    .sample {
      overflow: hidden;
      position: relative;
    }
    
    .sample img {
      display: block;
      height: auto;
      width: 100%;
    }
    
    /* 1 つ目のサンプル */
    .sample-one.sample-animation img {
      animation: sample-one 1.2s cubic-bezier(.4, 0, .2, 1);
    }
    
    @keyframes sample-one {
      0% {
        opacity: 0;
        transform: scale(1.2) translateY(24px);
      }
      32% {
        opacity: 0;
        transform: scale(1.2) translateY(24px);
      }
    }
    
    /* 2 つ目のサンプル */
    .sample-second.sample-animation {
      animation: sample-second-img 2s cubic-bezier(.4, 0, .2, 1);
    }
    
    .sample-second.sample-animation:before {
      animation: sample-second-before 2s cubic-bezier(.4, 0, .2, 1) forwards;
      background: #fff;
      bottom: 0;
      content: '';
      left: 0;
      pointer-events: none;
      position: absolute;
      right: 0;
      top: 0;
      z-index: 1;
    }
    
    @keyframes sample-second-img {
      0% {
        opacity: 0;
      }
    }
    
    @keyframes sample-second-before {
      100% {
        transform: translateX(100%);
      }
    }
    
    /* 3 つ目のサンプル */
    .sample-third.sample-animation:before,
    .sample-third.sample-animation:after {
      animation: 2s cubic-bezier(.4, 0, .2, 1) forwards;
      background: #fff;
      bottom: 0;
      content: '';
      pointer-events: none;
      position: absolute;
      top: 0;
      z-index: 1;
    }
    
    .sample-third.sample-animation:before {
      animation-name: sample-third-before;
      left: 0;
      right: 50%;
    }
    
    .sample-third.sample-animation:after {
      animation-name: sample-third-after;
      left: 50%;
      right: 0;
    }
    
    @keyframes sample-third-before {
      100% {
        transform: translateY(100%);
      }
    }
    
    @keyframes sample-third-after {
      100% {
        transform: translateY(-100%);
      }
    }
    • お手数をお掛けしました。ご確認頂き、ありがとうございます。原因を特定できました。

      結論から申し上げると、 iOS でアニメーションを動かすには Polyfill の intersection-observer.js が必要です。

      intersection-observer.js の使い方は、「 Cocoon の一覧リストに心地よいスクロールアニメーション」の「ステップ 1 : IE と Safari 、 iOS に対応するかを決める」にて書いております。

      iOS が未対応だと知りませんでした。 Chrome Platform Status で確認すると、 iOS 版 Chrome は未対応でした…。

      書き変えた後には、記事一覧のリストに心地よいスクロールアニメーションがスマホで動かなくなりました。

      上記の intersection-observer.js を子テーマの javascript.js の 1 番上に追加した上で、下記の CSS の 2 ヶ所の 0%from に書き換えると動くと思います。

      /* 1 ヶ所目 */
      @keyframes sample-one {
        0% {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
        32% {
          opacity: 0;
          transform: scale(1.2) translateY(24px);
        }
      }
      
      /* 2 ヶ所目 */
      @keyframes sample-second-img {
        0% {
          opacity: 0;
        }
      }

      Cocoon の高速化設定の「 CSS を縮小化する」を有効にすると、 0% の部分が全て削除されてしまうんです。 from を使うと、削除されません。

  6. Akiraさん、バッチリと動きました!

    iosでChrome ・ Firefox ・Safari すぺてokです。

    御親切にご指導ありがとうございました

    また、よろしくお願いします。

    • chan さんのおかげです。ご指摘がなければ、 iOS で動かないのは Safari だけとの誤った認識のままでした。ありがとうございました。

      こちらこそ、またよろしくお願い致します。

送信に失敗しました

ボットと判定された可能性があります。

大変お手数をおかけしますが、以下の内容をご確認の上、再度のコメントの送信をお願い申し上げます。