はじめに
多くのサイトでよく目にするのが、画面内に入った要素が動くアニメーション。
下にスクロールしていくと「画像などがふんわりと表示」「左や右から出てくる」「小さかったものが大きくなる」など、サイトを彩るアニメーションを見かけます。
このようなアニメーションは、以前は下記のようなスクロールイベントで実現していました。
// jQuery の場合
$(window).scroll(function() {
処理を書く
});
ただ、スクロールイベントは、ブラウザに負荷をかけてしまいます。というのも、スクロールをする度に、処理がなされてしまうためです。何も対策をしなければ、「重い」サイトになる弊害がありました。また、その対策には限度がありました。
そこで最近使われているのが、Intersection observer 。ブラウザに負荷をかけずに、スクロールで画面内に入った要素にアニメーションを指定できます。
Intersection observer とは?
Intersection observer は、交点を監視し、要素が交差した時にイベントを発生させる API です。
詳しくは、Intersection Observer API や Intersection Observer を用いた要素出現検出の最適化をご参考ください。
簡単に言えば「スクロールイベントよりパフォーマンスが上がる」「開発の手間が省ける」もの。
今回ご紹介するアニメーションのほかに、画像の遅延読み込みにも利用できます。loading="lazy"
に未対応のブラウザ向けのフォールバック用としても使用が可能です。
執筆時点で Intersection observer に対応しているブラウザは、Chrome・Firefox・Edge です(iOS 版はいずれも未対応)。
ただ、対応していないブラウザのために、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 を使った評価の高い遅延読み込み用ライブラリです。