スクロールでアニメーションさせる方法を教えて!
スクロールアニメーションは、スクロールイベントを使用する方法や、ライブラリなどを使用する方法もありますが、今回はブラウザが提供している Web APIIntersectionObserverを利用した方法をご初会します。
ウェブサイトをスクロールしているときに、要素がふわっと表示されたり、横からスライドしてきたりするアニメーションを見かけることはありませんか?
これらのスクロールアニメーションは、視覚的な魅力を高めるだけでなく、ユーザーに情報をわかりやすく伝えるための重要な手法です。
この記事では、こうした効果を簡単に実現するために役立つ JavaScriptのIntersectionObserver APIを使った実装方法を、基本から応用までわかりやすく解説します。
IntersectionObserverとは?
IntersectionObserverは、ウェブページ内の特定の要素が「画面内に表示されたかどうか」を監視するためのWeb APIです。
IntersectionObserverはこれを解決し、効率的かつ簡単に要素の表示・非表示を検知する仕組みを提供します。
従来はスクロールイベント(scroll イベント)を使って似たような機能を実装することが一般的でしたが、この方法はコードが複雑になりやすく、イベントが頻繁に発生するためパフォーマンスの問題もありました。
交差(Intersection)の判定
「交差」とは、監視対象の要素(ターゲット)と、可視範囲(画面や親要素の範囲)が重なる部分のことを指します。
IntersectionObserver は、「どれくらい交差しているか」(交差率や割合)を監視し、指定した条件が満たされたらイベントを発火させます。
仕組みを簡単に図で表すと以下の通りです。
IntersectionObserverイベントのサンプル
では、こちらがIntersectionObserverを使用したサンプルです。
スクロールして、画面の下部と、用意したカードが交差した時にアニメーションが発火します。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
<h1>下にスクロールしてみてね</h1>
<section class="cards">
<div class="card target">Card 1</div>
<div class="card target">Card 2</div>
<div class="card target">Card 3</div>
<div class="card target">Card 4</div>
<div class="card target">Card 5</div>
</section>
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.card {
background-color: rgb(255, 222, 224);
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 20px;
text-align: center;
font-size: 1.2rem;
/* イベントが発火する前は非表示 */
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease;
}
/* イベントが発生し、アニメーションが適用された時表示 */
.card.visible {
opacity: 1;
transform: translateY(0);
}
// IntersectionObserver コールバック関数
const observerCallback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible'); // 可視領域に入ったらクラスを追加
} else {
entry.target.classList.remove('visible'); // 可視領域から外れたらクラスを削除
}
});
};
// IntersectionObserver のオプション設定
const observerOptions = {
root: null, // ビューポートを基準
threshold: 0.2 // 20%が見えたら発火
};
// Observer を作成
const observer = new IntersectionObserver(observerCallback, observerOptions);
// 監視対象の要素を登録
document.querySelectorAll('.target').forEach(target => observer.observe(target));
コード解説
上記サンプルのコードを解説致します。
(1) オブザーバー(Observer)の作成
コールバック関数を指定
まず、IntersectionObserver の「コールバック関数」を設定します。
指定した要素がビューポートや親要素と交差するたびに、ブラウザが自動的に呼び出します。
// IntersectionObserver コールバック関数
const observerCallback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible'); // 可視領域に入ったらクラスを追加
} else {
entry.target.classList.remove('visible'); // 可視領域から外れたらクラスを削除
}
});
};
引数のentriesは、IntersectionObserver によって監視されているターゲット要素の状態を表すIntersectionObserverEntryオブジェクトの配列です。
IntersectionObserverEntryオブジェクトの主なプロパティは以下の通りです。
サンプルは、isIntersectingプロパティを利用してクラスの付け外しを行っています。
entry.isIntersecting | ターゲット要素が「可視範囲に入っているかどうか」を真偽値(true / false )で示します。 |
entry.target | 監視されているDOM要素そのものです。この要素に対してクラスの追加やスタイルの変更が可能です。 |
entry.intersectionRatio | ターゲット要素がどの程度交差しているか(見えている割合)を表します。 例えば、 0.5 なら要素の 50% が可視領域内にあります。 |
entry.boundingClientRect | ターゲット要素の位置やサイズを表すオブジェクト。 |
オプション(監視条件)を指定
IntersectionObserver
には、次のような便利な設定オプションがあります:
root | 監視基準となる要素。デフォルトはブラウザのビューポート |
rootMargin | 監視領域を拡張または縮小(CSS形式: 10px , -20% など) |
threshold | 要素がどれだけ表示されたら発火するかを割合で指定(サンプルでは20%) |
// IntersectionObserver のオプション設定
const observerOptions = {
root: null, // ビューポートを基準
threshold: 0.2 // 20%が見えたら発火
};
IntersectionObserverインスタンスの作成
作成したコールバック関数、オプションを引数として渡して、IntersectionObserverインスタンスを作成します。
// Observer を作成
const observer = new IntersectionObserver(observerCallback, observerOptions);
(2) 要素(ターゲット)の登録と監視の開始
監視したい要素を observe() メソッドで登録します。
// 「.target」クラスが付いている、監視対象の要素を全て登録
document.querySelectorAll('.target').forEach(
// 各要素を IntersectionObserver に登録し、監視を開始。
target => observer.observe(target)
);
observe()
observe()
は、IntersectionObserver
のメソッドのひとつで、監視対象の要素を登録するために使います。
このメソッドを使うことで、指定したDOM要素(ターゲット)が画面や特定のコンテナ内に「入る」または「出る」タイミングを監視するように設定できます。
observer.observe(target);
(3) 条件を満たしたときにコールバックを実行
指定した交差率(threshold)を超えると、コールバック関数が呼び出されます。
今回は先述の通り、画面に表示されたら監視要素である「.card .target」クラスに「.visible」を付与することで、画面に表示しています。
.card {
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease;
}
.card.visible {
opacity: 1;
transform: translateY(0);
}
IntersectionObserverイベント 応用サンプル
仕組みを解説したところで、さらに応用したサンプルをご紹介致します。
もっと画面に見えてからイベントを起こす場合
先ほどのサンプルは、画面と20%交差した時点でイベントが起こりました。
今度は、画面と8割交差した時点で色が変わるようにしたサンプルです。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
どれくらい交差した時にイベントを発火させるかは、先述した通りIntersectionObserver
のオプションであるthresholdで設定します。
// IntersectionObserver のオプション設定
const observerOptions = {
root: null,
threshold: 0.8 // 80%が見えたら発火
};
// Observer を作成
const observer = new IntersectionObserver(observerCallback, observerOptions);
一度だけイベントを起こしたい場合
これまでのサンプルは、画面に出たり、入ったりするたびにイベントが起こっていましたがイベントを一度だけに制限したい場合もあると思います。その場合のサンプルです。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
監視イベントを一度だけ発火させたい場合、コールバック関数内で unobserve() メソッドを使用して、監視対象を解除することができます。
// IntersectionObserver コールバック関数
const observerCallback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible'); // 可視領域に入ったらクラスを追加
observer.unobserve(entry.target); // 一度だけ発火させるため監視解除
}
});
};
unobserve()
unobserve() メソッドを使うと、指定した要素(entry.target
)の監視を停止します。これにより、該当の要素が再度スクロールで可視領域に入ってもコールバックは呼び出されません。
CSSアニメーションと連携する場合
これまでは、transition
を使用してのアニメーションでしたが、@keyframesを使用した実装ももちろん可能です。
@keyframesを使用することで、複雑なアニメーションも実現可能です。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
イベント発火時に付与している「.visible」クラスにanimationプロパティを指定することで登場アニメーションを実装しています。
/* アニメーション設定 */
@keyframes fancyFadeIn {
0% {
opacity: 0;
transform: translateY(50px) scale(0.8) rotate(-10deg); /* 初期状態 */
}
50% {
opacity: 0.5;
transform: translateY(20px) scale(1.1) rotate(5deg); /* 少し拡大+回転 */
}
100% {
opacity: 1;
transform: translateY(0) scale(1) rotate(0deg); /* 最終状態 */
}
}
/* 可視状態になったらアニメーションを適用 */
.card.visible {
animation: fancyFadeIn 0.8s ease forwards; /* アニメーション再生 */
background-color: rgb(218, 255, 224); /* 背景色を変更 */
}
様々な方向から登場させたい場合
複数のアニメーション(例:左右からのフェードイン、下からのフェードインなど)をクラスごとに適用することも可能です
サンプルではdataset属性を組み合わせて実現しています。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
以下のように、data-animation を使って各要素のアニメーションタイプを指定します。
<section class="cards">
<div class="card target" data-animation="fade-up">Card 1</div>
<div class="card target" data-animation="fade-left">Card 2</div>
<div class="card target" data-animation="fade-right">Card 3</div>
<div class="card target" data-animation="fade-up">Card 4</div>
<div class="card target" data-animation="fade-left">Card 5</div>
</section>
@keyframes を使って、上下左右からフェードインするアニメーションを定義します。
/* 初期状態 */
.card {
opacity: 0;
transform: translateY(20px); /* デフォルトは下からの動き */
transition: background-color 0.5s ease;
}
/* 上からフェードイン */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 左からフェードイン */
@keyframes fadeLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 右からフェードイン */
@keyframes fadeRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* アニメーションの適用 */
.card.fade-up {
animation: fadeUp 0.8s ease forwards;
}
.card.fade-left {
animation: fadeLeft 0.8s ease forwards;
}
.card.fade-right {
animation: fadeRight 0.8s ease forwards;
}
IntersectionObserver を使い、スクロール時に要素の data-animation 属性を参照して適切なクラスを付与します。
// IntersectionObserver コールバック関数
const observerCallback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const animationType = entry.target.dataset.animation; // data-animation の値を取得
entry.target.classList.add(animationType); // 対応するクラスを追加
observer.unobserve(entry.target); // 一度だけ実行する場合
}
});
};
インフィニットスクロールを作りたい場合
インフィニットスクロール(Infinite Scroll) は、ウェブサイトやアプリケーションで、ユーザーがページを下にスクロールするたびに次のコンテンツを自動的に読み込む仕組みを指します。
スクロールしていくと永遠にコンテンツが表示されます。
See the Pen Untitled by kokusyo (@kokusyo) on CodePen.
初期状態では、コンテンツを3表示し、「#loading」を持った読込中の文言を配置しています。
今回はこの「#loading」を監視対象にしているため、「読込中...」の文言が表示されると、イベントが発火するようになっています。
<div id="content-container">
<div class="content-item">
<h2>コンテンツアイテム 1</h2>
<p>これは初期のコンテンツです。</p>
</div>
<div class="content-item">
<h2>コンテンツアイテム 2</h2>
<p>スクロールして新しいコンテンツを読み込んでください。</p>
</div>
<div class="content-item">
<h2>コンテンツアイテム 3</h2>
<p>さらに下へスクロールしてください。</p>
</div>
</div>
<div id="loading" class="loading">読み込み中...</div>
JavaScript色々書いていますが、loadMoreContent()関数は、コンテンツを末尾に追加する処理であり、今回のメインではないため解説は省きます。
このloadMoreContent()関数をコールバック関数の中で実行することで、コンテンツを増やしています。
document.addEventListener('DOMContentLoaded', () => {
const contentContainer = document.getElementById('content-container');
const loadingIndicator = document.getElementById('loading');
let contentIndex = 4; // コンテンツアイテムの番号を管理
// 新しいコンテンツアイテムを追加する関数
const loadMoreContent = () => {
for (let i = 0; i < 3; i++) {
const newItem = document.createElement('div');
newItem.classList.add('content-item');
const newHeading = document.createElement('h2');
newHeading.textContent = `コンテンツアイテム ${contentIndex}`;
const newParagraph = document.createElement('p');
newParagraph.textContent = `これは動的に追加されたコンテンツアイテム ${contentIndex} です。`;
newItem.appendChild(newHeading);
newItem.appendChild(newParagraph);
contentContainer.appendChild(newItem);
contentIndex++;
}
};
// IntersectionObserverでローディングインジケーターを監視
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
loadMoreContent(); // 新しいコンテンツを追加
}
});
// ローディングインジケーターを監視
observer.observe(loadingIndicator);
});
まとめ
IntersectionObserver は、Webページで動的なエフェクトを効率的に実装するための非常に便利なツールです。以下のような用途に幅広く利用できます:
- スクロールアニメーション(フェードイン、スライドインなど)。
- 遅延読み込み(画像や動画のロード最適化)。
- インフィニットスクロール。
従来のスクロールイベントを使った方法よりも、パフォーマンスが高く、設定も柔軟です。この記事を参考にして、あなたのウェブサイトに魅力的なアニメーションを追加してみましょう!