JavaScriptのIntersectionObserverを使って可視領域に入った要素をふわっと表示させる
前田 大地
セブンシックスのダイチです。画面をスクロールして可視領域に入ると、要素がフワッと出てくるやつ、ありますよね。それを、JavaScriptのIntersection Observer APIを使ってやってみよう!というのが今回のお話です。
目次
あらすじ
私はこれまで、要素をふわっと出すやつには「Emergence.js」というライブラリを好んで使っていました。軽量かつjQuery不要のライブラリです。Emergence.jsに不便は感じていなかったのですが、心情的にスクロールイベントよりもIntersection Observer APIを使いたかったので、代替ライブラリを探すことにしました。良さそうなライブラリが見つからないまま、ヒットしたJavaScriptの解説記事ばかり読んでるうちに、自分で書いたほうが早いということに気が付きました。おい。
やりたいこと
- スクロールして要素が画面内に入ったらふわっと表示させる
- 一度表示された要素は、また消えたりせずに出しっぱなしにする
- スクロールイベントではなくIntersection Observer APIを使う
- IE11にも対応させる
完成したコード
とりあえず完成したコードを掲載します。「.e」というクラスの付いた要素を監視して、可視領域に入ったとき「.e-v」というクラスが付与される仕組みです。ふわっと表示させるアニメーションにはCSSを使います。
〜略〜 <style> .e { opacity: 0; } .e-v { animation: fadeIn 1s ease forwards; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY( 20px ); } 100% { opacity: 1; transform: translateY( 0 ); } } </style> 〜略〜 <div class="e"> <h1>見出し</h1> <p>テキスト</p> </div> 〜略〜 <script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script> <script> window.addEventListener('load', function(){ // IntersectionObserverの作成 const observer = new IntersectionObserver(function(entries) { for(let i = 0; i < entries.length; i++) { // 領域内なら処理を実行 if (entries[i].intersectionRatio <= 0) continue; showElm(entries[i].target); } },{ // オプション rootMargin: '-10% 0% -10% 0%' }); // 監視対象の追加 const elements = document.querySelectorAll('.e'); for(let i = 0; i < elements.length; i++) { observer.observe(elements[i]); } // 領域内に入ったとき実行する処理 function showElm(e) { e.classList.add('e-v'); observer.unobserve(e); } },false); </script> 〜略〜
コード全体の解説
ここからは、上記のコードを解説していきます。
2〜19行目
CSSの記述です。「.e」クラスの付いた要素を透明にしておき、「.e-v」が付いたらふわっと表示させます。
21〜24行目
HTMLの記述です。ふわっとさせたい要素に「.e」クラスを付けておきます。
26行目
IE11ではIntersection Observer APIが使えないので、それを使えるようにするためのコードを読み込んでいます。
27〜55行目
JavaScriptの記述です。「.e」クラスのついた要素を監視して、可視領域に入ったら「.e-v」クラスを付与します。いちおう今回のメインなので、もうちょっと詳しく解説していきます。
JSコードの詳しい解説
window.addEventListener('load', function(){ // IntersectionObserverの作成 const observer = new IntersectionObserver(function(entries) { for(let i = 0; i < entries.length; i++) { // 領域内なら処理を実行 if (entries[i].intersectionRatio <= 0) continue; showElm(entries[i].target); } },{ // オプション rootMargin: '-10% 0% -10% 0%' }); // 監視対象の追加 const elements = document.querySelectorAll('.e'); for(let i = 0; i < elements.length; i++) { observer.observe(elements[i]); } // 領域内に入ったとき実行する処理 function showElm(e) { e.classList.add('e-v'); observer.unobserve(e); } },false);
前述の全体コードからJavaScriptを抜き出したものが上記のコードです。
1行目
window.addEventListener('load', function(){ 〜 },false);
ページの読み込みが完了してから実行させるための記述です。コードの記述位置などが変わっても動くようにと念のため書いてありますが、なくても動きます。
3行目
// IntersectionObserverの作成 const observer = new IntersectionObserver(function(entries) { for(let i = 0; i < entries.length; i++) { // 領域内なら処理を実行 if (entries[i].intersectionRatio <= 0) continue; showElm(entries[i].target); } },{ // オプション rootMargin: '-10% 0% -10% 0%' });
IntersectionObserverを作成します。ターゲットの要素(まだこの時点では監視対象が指定されていません)が、ビューポートと交差するのを監視して、交差したら処理を実行します。実行されるコールバック関数の内容が3〜7行目です。
今回のケースでは、オプションでrootMarginを変更しています(10行目)。viewportから上下10%を差し引いたボックス内に要素が交差したときにコールバック関数が実行されます。つまり、画面の上下10%はまだ可視領域とはみなさず、それよりも内側に要素がきたときに、ふわっと表示されます。サイトのデザインによってはヘッダーやフッターなどが上下10%より内側にこない場合もあると思います。そういう要素は永遠に非表示になってしまうので、そのあたりは適宜調整してください。
可視領域に入った要素はentriesという配列に格納されてコールバック関数に渡されます。たいてい1つしか入ってきませんが、例えば、高さが同じ要素が3つ横並びになっていた場合、スクロールすると3要素が同時に可視領域に入るので、そのときは配列に要素が3つ入ってきます。そのため、forで配列内の要素それぞれに対して処理を行う必要があります。
entries[i].intersectionRatioという変数は、領域内に要素がどれくらい食い込んでいるかを0〜1で表してくれます。0は、要素が完全に領域外にあるということなので、そのときは処理をスキップさせます(5行目)。どうして領域外かどうかをわざわざここで判定する必要があるかというと、ターゲットとなる要素を監視対象に含めたタイミングで、一度コールバック関数が実行されてしまうからです(そういう仕様みたいです)。
15行目
// 監視対象の追加 const elements = document.querySelectorAll('.e'); for(let i = 0; i < elements.length; i++) { observer.observe(elements[i]); }
IntersectionObserverを作成した後に、監視する要素を登録します。今回は、「.e」というクラス名のついた要素をすべて監視します。
21行目
// 領域内に入ったとき実行する処理 function showElm(e) { e.classList.add('e-v'); observer.unobserve(e); }
可視領域に入ったときに実行する処理です。といっても、要素出現のアニメーションなどはCSSで行いますので、ここでやるのは「.e-v」というクラスを付与するだけです。今回は、処理は一度だけ行うものですから、クラスを付与した後、監視対象から除外します。
この処理は前述した仕様のおかげで、ターゲットとなる要素を監視対象に含めたタイミングでまず1度実行されます。ですので、ページが読み込まれたときに最初から可視領域にある要素は、すぐに「.e-v」が付与されて監視対象から除外されるというワケです。
要素によってアニメーションを変えたい場合
要素によって複数のアニメーションを使い分けたい場合などもあると思います。
その場合も、JavaScriptはそのまま同じものが使用できます。変更するのはCSSで、.eや.e-vに直接スタイルを適用するのではなく、別途アニメーションの種類に合わせたクラスを付与するのがカンタンな方法です。
〜略〜 <style> .fadeIn { opacity: 0; } .fadeIn.e-v { animation: fadeIn 1s ease forwards; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY( 20px ); } 100% { opacity: 1; transform: translateY( 0 ); } } </style> 〜略〜 <div class="e fadeIn"> <h1>見出し</h1> <p>テキスト</p> </div> 〜略〜
上記の例では「.fadeIn」というクラスの付いた要素に、フェードインのアニメーションを与えています。もし、フェードイン以外のアニメーションが必要になったら、新たなクラスを付与して、そのクラスに別のアニメーションを指定してあげればOKです。カンタンですね。
やってみた感想
ページが読み込まれたとき、まだ監視対象が可視領域にいないにもかかわらずコールバック関数が勝手に実行されるので、理由が分からずとっても焦りました。IntersectionObserverのインスタンスをインスタンス化すると、コールバックが発生するのが正しい仕様だそうです。この挙動を取り上げている記事がほぼゼロだったので、なかなか気付けませんでした。やはり自分で実際にやってみると、とても勉強になりますね。