Intersection observer によるスクロールアニメーション
はじめに
多くのサイトでよく目にするのが、画面内に入った要素が動くアニメーション。
下にスクロールしていくと「画像などがふんわりと表示」「左や右から出てくる」「小さかったものが大きくなる」など、サイトを彩るアニメーションを見かけます。
このようなアニメーションは、以前は下記のようなスクロールイベントで実現していました。
// jQuery の場合 $(window).scroll(function () { 処理を書く; });
ただ、スクロールイベントは、ブラウザに負荷をかけてしまいます。というのも、スクロールをする度に、処理がなされてしまうためです。何も対策をしなければ、重いサイトになる弊害がありました。また、その対策には限度がありました。
そこで最近使われているのが、Intersection observer 。ブラウザに負荷をかけずに、スクロールで画面内に入った要素にアニメーションを指定できます。
Intersection observer とは?
Intersection observer は、交点を監視し、要素が交差した時にイベントを発生させる API です。
詳しくは、Intersection Observer API や Intersection Observer を用いた要素出現検出の最適化 をご参考ください。
簡単に言えば「スクロールイベントよりパフォーマンスが上がる」「開発の手間が省ける」もの。
今回ご紹介するアニメーションのほかに、画像の遅延読み込みにも利用できます。loading="lazy"
ではなく、独自に画像の遅延読み込みをしたい時にも便利です。
参考:How do I handle browsers that don’t yet support lazy-loading?
実例
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 images = document.querySelectorAll('.sample'); const imageClassName = 'sample-animation'; const imageObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add(imageClassName); } else { entry.target.classList.remove(imageClassName); } }); }); images.forEach((image) => { imageObserver.observe(image); });
そして、最後に 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(0.4, 0, 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(0.4, 0, 0.2, 1); } .sample-second.sample-animation::before { animation: sample-second-before 2s cubic-bezier(0.4, 0, 0.2, 1) forwards; background: #fff; content: ''; inset: 0; pointer-events: none; position: absolute; 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(0.4, 0, 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 images = document.querySelectorAll('.sample'); const imageClassName = 'sample-animation'; const imageObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add(imageClassName); // ターゲット要素の監視を停止 imageObserver.unobserve(entry.target); } }); }); images.forEach((image) => { imageObserver.observe(image); });
IntersectionObserver.unobserve()
を使っています。class にsample-animation
を追加後は、要素の監視を停止します。
発動タイミングを遅らせる
これまでの 2 つの JavaScript は、いずれも要素が画面内に入ると、すぐにアニメーションが発動します。
アニメーションによっては、またはアニメーションを指定する対象によっては、アニメーションの発動を遅らせたい場合があります。Intersection observer のオプション rootMargin
を指定すれば、アニメーション発動のタイミングをコントロールできます。
rootMargin
を指定したサンプルをご覧ください。下の 3 つの円が画面内に入ってから、さらに 200px スクロールした地点でアニメーションが発動します。
3 つの円の HTML は、下記のとおりです。
<div class="sample-circles"> <div class="sample-circle sample-circle-one"></div> <div class="sample-circle sample-circle-second"></div> <div class="sample-circle sample-circle-third"></div> </div>
JavaScript 。
const circles = document.querySelector('.sample-circles'); const circleClassNames = 'sample-circle-animation'; const circleObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add(circleClassNames); } else { entry.target.classList.remove(circleClassNames); } }); }, { rootMargin: '-200px 0px', }, ); circleObserver.observe(circles);
CSS 。
.sample-circles { display: flex; justify-content: space-around; } .sample-circle { animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); background: #ff4081; border-radius: 50px; height: 80px; width: 80px; } .sample-circle-one { animation-duration: 0.3s; } .sample-circle-second { animation-duration: 0.4s; } .sample-circle-third { animation-duration: 0.5s; } .sample-circle-animation .sample-circle { animation-name: sample-circle-animation; } @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 を使った評価の高い遅延読み込み用ライブラリです。