Webサイト制作において、「スクロールして要素が画面に入ったらフワッと表示させる」という演出は定番です。
これまでは外部ライブラリを使うのが一般的でしたが、現在はブラウザ標準機能の「Intersection Observer(インターセクション・オブザーバー)」を使うのが主流です。
今回は、余計なファイルを読み込まず、サイトを軽量化できる「Intersection Observer」を使って、実務で頻出する「3つの発火タイミング」の作り分けをご紹介します。
Intersection Observer とは?
直訳すると「交差監視員」です。
「ブラウザの画面」と「監視したい要素」が交差したか(画面内に入ったか)を、非常に効率よく監視してくれる機能です。
今回はMDN公式の書き方に準拠しつつ、以下の3パターンを実装します。
- 通常(Immediate):少しでも画面に入ったら発火
- 遅延(Offset):画面に入って100px進んだら発火
- 完全表示(Threshold):要素の全体が画面に入りきったら発火
HTML/CSS
まずは共通のHTMLとCSSを用意します。
挙動の違いがわかるよう、3つの異なるデザインのボックスを用意しました。
HTML
監視対象(.box)に、それぞれの挙動用のクラス(.js-observer-default など)を付与します。
<div class="container">
<!-- ① 通常(すぐ発火) -->
<div class="box box-gray js-observer-default">
<span>パターン1</span><br>(すぐ出る)
</div>
<!-- ② オフセットあり(100px遅延) -->
<div class="box box-pink js-observer-offset">
<span>パターン2</span><br>(100px待つ)
</div>
<!-- ③ 完全表示(要素全体が画面に入りきったら) -->
<div class="box box-blue js-observer-threshold">
<span>パターン3</span><br>(要素全体が画面に入りきったら)
</div>
</div>
CSS
初期状態は透明(opacity: 0)で、少し下に配置しておき、クラス is-show が付いたらフワッと浮き上がります。
/* レイアウト用(スクロールさせるための余白) */
.container {
margin: 100vh 0;
display: flex;
align-items: center;
justify-content: center;
gap: 100px; /* 間隔を広めに */
}
/* ボックスの共通スタイル */
.box {
width: 200px;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-weight: bold;
border-radius: 10px;
/* アニメーション設定(初期状態) */
opacity: 0;
transform: translateY(50px);
transition: all 0.8s ease;
}
/* ① 通常ボックス(実線・グレー) */
.box-gray {
border: 4px solid #555;
background-color: #f0f0f0;
color: #333;
}
/* ② 遅延ボックス(点線・ピンク) */
.box-pink {
border: 4px dashed #e91e63;
background-color: #fff0f5;
color: #e91e63;
}
/* ③ 完全表示ボックス(二重線・青) */
.box-blue {
border: 6px double #2196f3;
background-color: #e3f2fd;
color: #1565c0;
}
/* 画面内に入った時に付与するクラス */
.is-show {
opacity: 1;
transform: translateY(0);
}
JavaScript(3パターンの実装)
ここがメインです。公式(MDN)スタイルに準拠し、設定(options)を分けて記述しています。
document.addEventListener("DOMContentLoaded", function () {
// ==============================================
// 共通の動き(クラスを付ける関数)
// ==============================================
const doWhenIntersect = function (entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("is-show");
observer.unobserve(entry.target); // 監視終了
}
});
};
// ==============================================
// パターン1:通常(Immediate)
// ==============================================
// 少しでも画面に入ったら発火
const optionsDefault = {
root: null,
rootMargin: "0px",
threshold: 0
};
const observerDefault = new IntersectionObserver(doWhenIntersect, optionsDefault);
// 監視する要素を指定して実行
document.querySelectorAll(".js-observer-default").forEach(el => {
observerDefault.observe(el);
});
// ==============================================
// パターン2:遅延(Offset)
// ==============================================
// 画面の下から100px入ってから発火
const optionsOffset = {
root: null,
rootMargin: "0px 0px -100px 0px", // 下を100px縮める
threshold: 0
};
const observerOffset = new IntersectionObserver(doWhenIntersect, optionsOffset);
// 監視する要素を指定して実行
document.querySelectorAll(".js-observer-offset").forEach(el => {
observerOffset.observe(el);
});
// ==============================================
// パターン3:完全表示(Threshold 1.0)
// ==============================================
// 要素の全体(100%)が画面に入りきったら発火
const optionsThreshold = {
root: null,
rootMargin: "0px",
threshold: 1.0 // 1.0 = 100%
};
const observerThreshold = new IntersectionObserver(doWhenIntersect, optionsThreshold);
// 監視する要素を指定して実行
document.querySelectorAll(".js-observer-threshold").forEach(el => {
observerThreshold.observe(el);
});
});
実装パターンB:実務で使える「短縮コード」
「似たようなコードを何度も書きたくない」という場合は、こちらの関数化パターンがおすすめです。
「要素のクラス名」と「オプション」を渡すだけで監視をスタートできる関数 registerObserver を作ることで、記述量が半分以下になります。
document.addEventListener("DOMContentLoaded", function () {
/**
* 監視機能を作成して実行する関数(共通化)
* @param {string} selector 監視したい要素のクラス名
* @param {object} options 監視のオプション
*/
const registerObserver = (selector, options = {}) => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("is-show");
observer.unobserve(entry.target);
}
});
}, options);
// 指定された要素すべてに対して監視を開始
document.querySelectorAll(selector).forEach(el => observer.observe(el));
};
// ==========================================
// 実行部分はこれだけ!
// ==========================================
// ① 通常(オプション省略時はデフォルトが適用されます)
registerObserver(".js-observer-default");
// ② 遅延(必要なオプションだけ渡す)
registerObserver(".js-observer-offset", {
rootMargin: "0px 0px -100px 0px"
});
// ③ 完全表示
registerObserver(".js-observer-threshold", {
threshold: 1.0
});
});
実務ではこの「パターンB」を使っておくと、後からアニメーション要素が増えた時も1行追加するだけで済むので非常に便利です。
解説:設定値の使い分け
Intersection Observerの挙動を決めるのは、主に2つのプロパティです。
1. rootMargin(判定エリアの調整)
「画面の判定ラインをどこにするか」を決めます。
"0px":画面の端ちょうどで判定します。"0px 0px -100px 0px":画面の下端を100px内側に縮めます。これにより「100pxスクロールしないと反応しない」遅延効果を作れます。
2. threshold(交差率の閾値)
「要素の何%が見えたらOKとするか」を決めます。
- 0 → 0%を超えたら(=1ピクセルでも見えたら)発火。
- 1.0 → 100%(=全体)が見えたら発火。
- 0.5 → 半分見えたら発火。
threshold: 1.0 は「要素の全体が画面内に入りきった瞬間」に発火します。
「スマホの画面の高さ」よりも「要素の高さ」の方が大きい場合、物理的に全体が入りきらないため、永遠にアニメーションが発火しないという問題が発生します。
縦に長いコンテンツ(記事本文全体など)には使わず、ボタンや見出しなど「画面に収まる小さな要素」に使用してください。
よくある質問(FAQ)
- 「in-view.js」や「inview.js」とどれくらいパフォーマンスが違うのでしょうか?
-
従来のライブラリ(in-view.jsなど)は、スクロールのたびに高頻度で計算を行う「scrollイベント」を監視するため、スクロールのカクつきの原因になっていました。
一方、標準API(Intersection Observer)はブラウザの内部システムで最適に管理されており、「ターゲットが重なった瞬間」だけを通知してくれるため、CPU負荷を最小限に抑えられます。外部ファイルやjQueryへの依存もなくなるため、ページの読み込み速度の改善にも直結します。 - rootMargin は「%」で指定できますか?
-
はい、可能です。
rootMargin: "0px 0px -20% 0px"のように指定することで、画面の高さに応じた柔軟な判定エリアを作れます。
特に、画面サイズの異なるスマホとPCを跨ぐ実装では、px指定よりも%指定の方が意図したタイミングで発火させやすいためおすすめです。 - スマホだけ設定を変えることはできますか?
- 可能です。
window.matchMediaを利用し、デバイスの切り替わりをフラグで管理することで、レスポンシブに設定を適用できます。以下は、jQueryを使用した実務的な切り替えコードの例です。$(function () { var deviceflag = ''; var observer; // 共通の動き(クラスを付ける関数) const doWhenIntersect = function (entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add("is-show"); observer.unobserve(entry.target); } }); }; $(window).on('load resize', function () { var w = window.matchMedia('(max-width: 750px)').matches; // SP幅(750px以下) if (w === true && deviceflag != 'sp') { deviceflag = 'sp'; if (observer) observer.disconnect(); // 以前の監視を解除 observer = new IntersectionObserver(doWhenIntersect, { rootMargin: "0px" }); document.querySelectorAll(".js-target").forEach(el => observer.observe(el)); // PC幅 } else if (w === false && deviceflag != 'pc') { deviceflag = 'pc'; if (observer) observer.disconnect(); // 以前の監視を解除 observer = new IntersectionObserver(doWhenIntersect, { rootMargin: "0px 0px -100px 0px" }); document.querySelectorAll(".js-target").forEach(el => observer.observe(el)); } }); });


