スクロールして画面内に入った要素が「ふわっ」と表示される演出は、多くのサイトで使われています。かつては scroll イベントで実装していましたが、現在はIntersectionObserver を使うことで、軽量かつ高パフォーマンスに実現できます。
opacity: 0 にしておき、IntersectionObserver で画面に入ったら .active クラスを付けてCSSの transition でフェードアップさせます。一度きりなら unobserve、prefers-reduced-motion への配慮も加えると親切です。完成イメージ(動くデモ)
↓ このあたりをゆっくりスクロールすると、ボックスがふわっと現れます:
IntersectionObserverとは?
IntersectionObserver は、指定した要素がビューポートに入ったかを効率的に監視するAPIです。scroll イベントを多用せずに済むため、パフォーマンスに優れています。基本的な使い方はIntersectionObserverで複数要素を監視する方法で解説しています。
HTMLの準備
フェードインさせたい要素に共通のクラス(ここでは fade-in)を付けます。
<div class="fade-in">コンテンツ1</div> <div class="fade-in">コンテンツ2</div> <div class="fade-in">コンテンツ3</div>
CSSでフェードアップを定義
初期状態は opacity: 0 +少し下にずらし(translateY)、.active が付いたら元の位置・不透明に transition させます。
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.active {
opacity: 1;
transform: translateY(0);
}
JavaScriptでIntersectionObserverを設定
対象要素を監視し、画面に入った(isIntersecting)ら .active を付与します。一度表示したら unobserve で監視を解除します。
document.addEventListener("DOMContentLoaded", () => {
const targets = document.querySelectorAll(".fade-in");
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("active");
obs.unobserve(entry.target); // 一度きり
}
});
}, {
threshold: 0.1, // 10%見えたら発火
});
targets.forEach((t) => observer.observe(t));
});
一度だけ&しきい値の調整
unobserve:一度表示したら監視を解除(無いと出入りのたびに再発火)threshold:どれくらい見えたら発火するか(0.1=10%)rootMargin: "0px 0px -100px 0px":「画面に入る少し手前」で発火させたいときに調整
順番に少しずつ表示する(stagger)
複数の要素を少しずつ時間をずらして表示すると、グッと洗練された印象になります(AOSなどのライブラリでお馴染みの演出)。各要素にインデックスに応じた transition-delay を設定するだけです。上のデモも、この方法でずらして表示しています。
const targets = document.querySelectorAll(".fade-in");
targets.forEach((el, i) => {
el.style.transitionDelay = `${i * 0.12}s`; // 0.12秒ずつ遅らせる
observer.observe(el);
});
CSSの :nth-child でも遅延をつけられますが、要素数が可変なら上記のようにJavaScriptで付ける方が柔軟です。
応用:毎回アニメ・別の動きにする
用途に合わせて、ふるまいや動きを変えられます。
- 画面に入るたび毎回アニメさせたいなら、
unobserveせず、isIntersectingで.activeを付け外しします(出入りのたびに再生) - 横からスライド:CSSの
transformをtranslateX(40px)に変える(詳しくは画面の横からフェードインさせる方法) - ズームしながら表示:
transform: scale(0.9)→scale(1)に変える
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// 入ったら付ける/出たら外す(毎回再生される)
entry.target.classList.toggle("active", entry.isIntersecting);
});
}, { threshold: 0.1 });
prefers-reduced-motionに配慮する
動きが苦手なユーザーのために、「視差効果を減らす」設定ではアニメーションせず最初から表示するようにします。
@media (prefers-reduced-motion: reduce) {
.fade-in {
transition: none;
opacity: 1;
transform: none;
}
}
関連するアニメーション
- 複数要素の監視やオプションの詳細 → IntersectionObserverで複数要素を監視する方法
- 読み込み時(スクロール不要)にフェードイン → 要素をふわっとフェードインさせる方法
- 横からスライドしながら表示 → 画面の横からフェードインさせる方法
- 要素が画面内にあるか判別する → 要素が画面内にあるかどうかを判別する方法
よくある質問(FAQ)
IntersectionObserver で要素が画面に入ったときにCSSクラス(.active)を追加し、CSSで opacity や transform の transition を定義します。コールバック内で unobserve すると一度だけアニメーションします。observer.unobserve(entry.target) を呼び、その要素の監視を解除します。これをしないと、スクロールで出入りするたびに繰り返しアニメーションします。threshold(見えた割合)と rootMargin(基準の余白)で調整します。rootMargin をマイナスにすると「画面に少し入ってから」発火させられます。@media (prefers-reduced-motion: reduce) で transition を無効化し、最初から表示する(opacity: 1)ようにします。アクセシビリティの基本配慮です。transition-delay をインデックスに応じて設定します。el.style.transitionDelay = `${i * 0.1}s` のように少しずつ遅らせると、順番にずれて表示される stagger 演出になります。まとめ
スクロール時のふわっと表示は、初期状態を opacity: 0 にしておき、IntersectionObserver で画面に入ったら .active を付けてCSSの transition でアニメーションさせるのが定番です。
一度きりなら unobserve、発火位置は threshold / rootMargin で調整。prefers-reduced-motion への配慮も忘れずに、軽快で親切な演出にしましょう。

