スクロールイベントとは何か
スクロールイベントは、ページや特定のスクロール可能な要素のスクロール位置が変わった瞬間に発火する合図です。window(ページ全体)にも、overflow: auto などでスクロール可能にした要素にも付けられます。ここが重要です:スクロールは高頻度で連続発火します。必ず軽い処理にし、必要なら間引き(throttle / debounce)や requestAnimationFrame を使って滑らかさを保ちます。
基本の使い方(window と要素)
ページ全体のスクロールを監視する例
<div style="height:2000px"></div>
<script>
window.addEventListener("scroll", () => {
const y = window.scrollY; // ページ上端からのスクロール量
const vh = window.innerHeight; // ビューポート高さ
console.log("scrollY:", y, "viewport:", vh);
}, { passive: true });
</script>
HTMLここが重要です:window のスクロールは { passive: true } を基本にします。これによりブラウザが最適化し、カクつきが減ります。スクロールを“止める”必要がある特殊な場面だけ passive を外します。
特定の要素に対してスクロールを監視する例
<div id="panel" style="height:140px; overflow:auto; border:1px solid #ccc">
<div style="height:600px"></div>
</div>
<script>
const panel = document.getElementById("panel");
panel.addEventListener("scroll", () => {
console.log("top:", panel.scrollTop, "height:", panel.clientHeight);
}, { passive: true });
</script>
HTMLここが重要です:要素のスクロール位置は scrollTop、表示領域は clientHeight、コンテンツ全体の高さは scrollHeight で取得します。ページ単位とはプロパティが異なる点に慣れましょう。
スクロール位置と寸法の取り方(正確な割合計算)
進捗割合の計算(ページ全体)
<script>
function getProgress() {
const y = window.scrollY;
const vh = window.innerHeight;
const h = document.documentElement.scrollHeight;
const max = Math.max(h - vh, 1); // 0除算を避ける保険
return Math.min(y / max, 1);
}
window.addEventListener("scroll", () => {
console.log("progress:", (getProgress() * 100).toFixed(1), "%");
}, { passive: true });
</script>
HTMLここが重要です:割合は「現在位置 ÷ 到達可能な最大量」。viewport 高さを引くのを忘れると 100% に達しません。0 で割らない保険も入れておくと安全です。
要素内の割合(水平・垂直どちらでも同じ考え方)
<script>
function getElementProgress(el) {
const top = el.scrollTop;
const visible = el.clientHeight;
const total = el.scrollHeight;
const max = Math.max(total - visible, 1);
return Math.min(top / max, 1);
}
</script>
HTMLここが重要です:水平の場合も scrollLeft / clientWidth / scrollWidth に読み替えるだけです。“見えている領域を引く”のがコアの考え方です。
パフォーマンスの基礎(passive / throttle / rAF)
監視の原則と最小コスト化
スクロールは毎フレーム級の頻度で発火するため、重い処理を直書きすると体感が悪化します。ここが重要です:基本は { passive: true }。重い処理は requestAnimationFrame(次の描画タイミングでまとめて実行)や throttle(一定間隔のみ実行)で間引きます。
requestAnimationFrame を使う例(描画タイミングでまとめる)
<script>
let ticking = false;
function onScrollFrame() {
ticking = false;
const y = window.scrollY;
// ここで軽い描画更新(例えば進捗バーの幅更新)
}
window.addEventListener("scroll", () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(onScrollFrame);
}
}, { passive: true });
</script>
HTMLここが重要です:フラグで1フレームに1回だけ更新。計算・描画を“同期”するとチラつきが減ります。
throttle の簡易例(一定間隔でのみ処理)
<script>
let last = 0;
const wait = 100; // ms
window.addEventListener("scroll", () => {
const now = performance.now();
if (now - last < wait) return;
last = now;
// ここで軽い処理
}, { passive: true });
</script>
HTMLここが重要です:入力遅延の許容範囲に応じて wait を調整します。UI の体感に合わせた間引きが鍵です。
代表的な実装例(ヘッダの影、進捗バー、無限スクロール)
ヘッダの影をスクロール開始時に付ける
<header id="head" style="position:sticky; top:0; background:white">ヘッダ</header>
<script>
function syncShadow() {
head.classList.toggle("shadow", window.scrollY > 0);
}
window.addEventListener("scroll", syncShadow, { passive: true });
syncShadow();
</script>
HTMLここが重要です:スクロールが 0 より大きいかどうかでクラスを切り替えるだけ。軽い分岐と最小のスタイル変更に留めます。
ページ上部に進捗バーを表示
<div id="bar" style="position:fixed;top:0;left:0;height:3px;background:#09f;width:0"></div>
<script>
function updateBar() {
const h = document.documentElement.scrollHeight;
const vh = window.innerHeight;
const max = Math.max(h - vh, 1);
const w = Math.min(window.scrollY / max, 1) * 100;
bar.style.width = w + "%";
}
window.addEventListener("scroll", () => requestAnimationFrame(updateBar), { passive: true });
updateBar();
</script>
HTMLここが重要です:計算は最小限、反映は rAF 内で一発。進捗の視認性が高く、コストが低い作りです。
無限スクロール(下端近くで追加読み込み)
<ul id="list"></ul>
<script>
let loading = false;
function nearBottom() {
const h = document.documentElement.scrollHeight;
const vh = window.innerHeight;
const y = window.scrollY;
return y + vh > h - 200; // 下端200px手前
}
async function loadMore() {
if (loading || !nearBottom()) return;
loading = true;
await new Promise(r => setTimeout(r, 400)); // 代わりに fetch など
const frag = document.createDocumentFragment();
for (let i = 0; i < 20; i++) {
const li = document.createElement("li");
li.textContent = "項目";
frag.appendChild(li);
}
list.appendChild(frag);
loading = false;
}
window.addEventListener("scroll", () => {
if (!loading && nearBottom()) loadMore();
}, { passive: true });
</script>
HTMLここが重要です:境界の“ニアミス”で反応し、フラグで重複ロードを防ぎます。大量追加は DocumentFragment で一括にすると軽くなります。
IntersectionObserver の活用(スクロール監視の代替)
ビューポートに入ったら処理する例
<div id="sentinel"></div>
<script>
const io = new IntersectionObserver((entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
console.log("見えたら処理");
// 画像遅延ロードや追加読み込みなど
}
});
});
io.observe(document.getElementById("sentinel"));
</script>
HTMLここが重要です:スクロールイベントを自前で処理せず、ブラウザが“見えた・消えた”を通知してくれる仕組みを使うと、コードが短く性能も安定します。無限スクロールの「境界検知」には特に有効です。
よくある落とし穴と注意点
スクロール処理に重い計算や DOM 書き換えを大量に入れると体感が悪化します。まず passive を付け、rAF や throttle で間引き、描画更新はクラス切り替えなど最小限に抑えます。スクロール抑止(preventDefault)は UX を損ねやすいので、モーダル中のロックなど必要最小限に限定します。位置計算のときは viewport(window.innerHeight)とコンテンツ(scrollHeight)を正しく区別し、割合の計算で“見えている領域を引く”のを忘れないようにします。スマホではタッチスクロールが主体で、ホイールや dblclick の体験が異なるため、タッチ向けの設計(スナップ、慣性など)も意識します。
まとめ
スクロールイベントは高頻度に発火するため、軽さが命です。window と要素で扱いを分け、位置は scrollY/scrollTop、寸法は innerHeight/clientHeight/scrollHeight で正確に取る。パフォーマンスは { passive: true } を基本にし、requestAnimationFrame や throttle で間引く。典型パターンは“影の付与”“進捗バー”“無限スクロール”。監視は IntersectionObserver を使うとシンプルで高速になる。これらを守れば、初心者でも滑らかで意図通りのスクロール連動 UI を安全に実装できます。
