ブラウザにやさしくスクロールで画面に入った要素にアニメーション

Intersection observerを使い可視範囲に入った要素にアニメーション

Akira

福岡在住ウェブデザイナー。 WordPress のカスタマイズを、「見やすさ」と「使いやすさ」にこだわり紹介しています。

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

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

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

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

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

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

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

Intersection observer とは?

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

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

今回ご紹介するアニメーションのほかに、画像の遅延読み込みにも利用できます。

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

各ブラウザのIntersection observerへの対応状況Can I use… : IntersectionObserver

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

Intersection observer の実例

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

早速 3 枚の画像によるサンプルをご覧ください。 JavaScript が使えない AMP では、動きません。

Intersection observerのサンプル画像

Intersection observerのサンプル画像

Intersection observerのサンプル画像

サンプルの HTML は、下記のとおりです。

<p class="sample sample-one"><img src="#" alt=""></p>
<p class="sample sample-second"><img src="#" alt=""></p>
<p class="sample sample-third"><img src="#" alt=""></p>

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

( () => {
    // 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' );
            }
        });
    });
    
    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 を下記のコードに変更します。

( () => {
    // 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' );
                // ターゲット要素の監視を停止
                observer.unobserve( entry.target );
            }
        });
    });
    
    sample.forEach( img => {
        observer.observe( img );
    });
})();

10 行目に 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 を使った遅延読み込み用ライブラリです。