下スクロールで消える・上スクロールで現れる JavaScript
動作
サイト上部に固定したヘッダーやナビなどが、下にスクロールすると消え、上にスクロールすると現れる JavaScript の紹介です。可能な限りサイトが重くならないように Throttle を使います。
実装後の動作は、このようなものです。サイト上部に、高さ 56px・背景色が赤のヘッダーを作っています。そのヘッダーが下スクロール時に消え、上スクロール時に現れます。
コード
今回は、ヘッダーを例とします。
HTML は単純なものです。
<body> <header>サイト名やナビなどを書く</header> </body>
JavaScript は、スクロールイベントを使います。スクロールイベントは高い頻度で発生するため、ブラウザに大きな負荷がかかります。その負荷を緩和するために、呼び出す関数の実行を一定時間あたり 1 回に間引く Throttle を使います。
Throttle を自分で書くこともできますが、今回は throttle-debounce を使用します。使用する方法は、以下の 3 つのいずれかです。
- throttle-debounce を npm でインストールし、モジュールをインポートする
- throttle-debounce/throttle.js を何かしらのファイルにそのままコピーし、モジュールをインポートする
- throttle-debounce/throttle.js の
function
以降をコピーし関数名をthrottle
にする
// 3 番目の function 以降をコピーする場合は関数名を throttle にします function throttle(delay, callback, options) { // あとは変更する必要はありません }
そして、JavaScript を書きます。今回は throttle-debounce/throttle.js の function
以降をコピーしたと仮定します。
// コピーしたthrottle-debounce/throttle.js function throttle(delay, callback, options) { // コピーした内容 } const { body } = document; const className = 'scroll-down'; let previousScrollY = 0; const removeBodyClass = () => body.classList.remove(className); const toggleBodyClass = () => { const currentScrollY = window.scrollY; if (currentScrollY > 36) { if (currentScrollY > previousScrollY + 80) { body.classList.add(className); } else if (currentScrollY < previousScrollY - 20) { removeBodyClass(); } } else { removeBodyClass(); } previousScrollY = currentScrollY; }; window.addEventListener('scroll', throttle(300, toggleBodyClass), false);
下にスクロールをすれば <body>
の class に scroll-down
を追加します。上にスクロールをすれば、その scroll-down
を class から削除します。また、このような動作をします。
- サイト最上部より 36px 以上スクロールした地点から
scroll-down
を追加する。それより上の地点では必ずscroll-down
を削除する。 - 下へのスクロール量が 80px を超える場合に
scroll-down
を追加する。 - 上へのスクロール量が 20px を超える場合に
scroll-down
を削除する。
この動きは、AMP の amp-fx-collection と同じです。AMP はユーザー体験を重視しているため、参考にしました。
あとは CSS を書けば終わりです。
header { background: #fff; box-shadow: 0 4px 8px -3px rgb(17 17 17 / 6%); height: 56px; position: fixed; top: 0; transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); width: 100%; z-index: 10; } .scroll-down header { translate: 0 -100%; }
<header>
に position: fixed;
と top: 0;
を指定し、サイト上部に固定します。
そして、JavaScript で追加する class の scroll-down
を使い、translate: 0 -100%;
を指定します。これで下にスクロール時は<header>
が上に消えていきます。要素の高さを使った top: -56px;
でも同じ表現はできますが、GPU アクセラレーションが可能な translate
または transform: translate()
での指定がおすすめです。ブラウザの負荷を抑えられる上に、アニメーションが滑らかになります。
requestAnimationFrame() は要注意
以前まで、この記事には requestAnimationFrame()
を使った JavaScript を書いていました。
let offset = 0; let lastPosition = 0; let ticking = false; const header = document.getElementById('header'); const height = 56; const onScroll = () => { if (lastPosition > height) { if (lastPosition > offset) { header.classList.add('head-animation'); } else { header.classList.remove('head-animation'); } offset = lastPosition; } }; document.addEventListener('scroll', () => { lastPosition = window.scrollY; if (!ticking) { window.requestAnimationFrame(() => { onScroll(); ticking = false; }); ticking = true; } });
これは Google のエンジニアさんが書いた記事を参考にしていました。現在は別のページにリダイレクトされますが、Internet Archive に残っている Leaner, Meaner, Faster Animations with requestAnimationFrame で確認できます。
ただ、requestAnimationFrame()
は意味がありませんでした。例えば、このように書いてみます。
let ticking = false; document.addEventListener('scroll', () => { if (!ticking) { window.requestAnimationFrame(() => { ticking = false; console.log('処理しました'); }); ticking = true; } else { console.log('処理しませんでした'); } });
どんなにスクロールしようともコンソールに 処理しませんでした
とは出力されません。必ず ticking
は false
になります。理由は、MDN の Document: scroll イベント に書かれています。
入力イベントやアニメーションフレームは同じような割合で発生するため、そのため下記のような最適化は不要の場合が多いことに注意してください。
requestAnimationFrame()
を同じように使った最適化は不要な場合が多いと言っています。
そのため、Throttle や Debounce を使うのがいいように思えます。
passive: true は意味がない
スクロールのパフォーマンスを改善するために、addEventListener
の引数に { passive: true }
を付けるといいと時々見かけます。
document.addEventListener( 'scroll', () => { // 何かしらの処理 }, { passive: true }, );
これはスクロールイベントには意味がありません。スクロールイベントはキャンセルができないためです。
パッシブイベントリスナーが Chrome 51 で実装された時に、 パッシブ イベント リスナーによるスクロール パフォーマンスの改善 で Google が言及しています。
注: 基本的な scroll イベントはキャンセルできないため、パッシブに設定する必要はありません。ただし、ハンドラで高コストの作業が完了するのを防ぐ必要があります。