このサイトのナビは、少しスクロールすると背景にぼかしと影が付きます。演出としては地味ですが、実装方法をあえてシンプルに保った判断について書きます。
scrollY > 40 を toggle するだけ
function initNav(): void {
const nav = document.getElementById("nav");
if (!nav) return;
const onScroll = () => {
nav.classList.toggle("scrolled", window.scrollY > 40);
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
}
見た目のスクロール演出というと IntersectionObserver を反射的に使いたくなりますが、このサイトのスクロールリベール演出(別記事で紹介)とは性質が違います。リベールは「複数の要素それぞれが画面に入ったかどうか」を判定する必要がある一方、ナビの背景切り替えは「ページ全体が何pxスクロールしたか」という単一のグローバルな値だけで決まります。監視対象がそもそも複数の要素の交差ではなく、1つの数値のしきい値判定なので、IntersectionObserver を持ち出すのは過剰設計だと判断しました。
passive: true で描画をブロックしない
window.addEventListener("scroll", onScroll, { passive: true });
scroll イベントは非常に高頻度で発火するため、リスナー内で preventDefault() を呼ぶ可能性がないなら passive: true を必ず付けるべきです。これによりブラウザはスクロールのレンダリングをイベントハンドラの完了を待たずに進められるため、体感のスクロール滑らかさに直結します。
初回呼び出しを忘れない
onScroll() を登録直後に一度手動で呼んでいるのもポイントです。ページをスクロール済みの状態でリロードした場合(ブラウザの「戻る」操作など)、scroll イベントは発火しないことがあるため、初期状態のチェックを明示的に行わないと、ナビの背景が本来付くべきなのに付いていない、というズレが生じます。
「凝った実装」より「その場に合った実装」
このサイトには IntersectionObserver を使った演出(スクロールリベール)と、素朴な scroll イベントを使った演出(ナビの背景)が共存しています。どちらも「正しい」実装で、選ぶ基準は「監視対象が複数要素の交差なのか、単一のグローバルな値なのか」という一点です。新しいAPIを使うこと自体を目的にせず、要件の形に実装の形を合わせる、という当たり前のことを再確認した実装でした。