【完全版】スムーススクロールの実装は全てこれでいい【LazyLoad対応】
コードを見直し細かい箇所をリファクタリングしました。主要ブラウザ全てで動作確認済みです。
JavaScriptを使った、全部入りのスムーススクロールの実装方法をご紹介します。
固定ヘッダーによる重なり防止や、Lazy Load(遅延読み込み)による位置ずれ対策も加え、ほぼ困らないようなスムーススクロールになっているのではないでしょうか。
ページ内のスクロールはもちろん、別ページ遷移後にスクロールさせることもできます。
イージングにもこだわり、操作感の強いeaseOutExpoを採用しました。
ベースとなるコード
カスタマイズ前の標準的なスムーススクロールのコードです。(シンプルに実装したい場合はこちらでもOK)
// 固定ヘッダー(固定しない場合は = 0)
const headerHeight = document.querySelector('header').offsetHeight + 20;
// ページ内のスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
link.addEventListener('click', (e) => {
const hash = e.currentTarget.hash;
const target = document.getElementById(hash.slice(1));
// ページトップへ("#"と"#top")
if (!hash || hash === '#top') {
e.preventDefault();
window.scrollTo({
top: 1, // iOSのChromeで固定ヘッダーが動くバグがあるため0ではなく1に
behavior: 'smooth',
});
// アンカーへ
} else if (target) {
e.preventDefault();
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo({
top: position,
behavior: "smooth",
});
// URLにハッシュを含める
history.pushState(null, '', hash);
}
});
};
// 別ページ遷移後にスムーススクロール
const urlHash = window.location.hash;
if (urlHash) {
const target = document.getElementById(urlHash.slice(1));
if (target) {
// ページトップから開始(ブラウザ差異を考慮して併用)
history.replaceState(null, '', window.location.pathname);
window.scrollTo(0, 0);
window.addEventListener("load", () => {
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo({
top: position,
behavior: "smooth",
});
// ハッシュを再設定
history.replaceState(null, '', window.location.pathname + urlHash);
});
}
}
jQuery版はこちら
完全版スムーススクロールのコード
以下は、ベースとなるコードにイージングと遅延読み込みによる位置ずれ対策を加えたものです。
// 固定ヘッダー(固定しない場合は = 0)
const headerHeight = document.querySelector('header').offsetHeight + 20;
// イージング関数(easeOutExpo)
function scrollToPos(position) {
const startPos = window.scrollY;
const distance = Math.min(position - startPos, document.documentElement.scrollHeight - window.innerHeight - startPos);
const duration = 800; // スクロールにかかる時間(ミリ秒)
let startTime;
function easeOutExpo(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}
function animation(currentTime) {
if (startTime === undefined) {
startTime = currentTime;
}
const timeElapsed = currentTime - startTime;
const scrollPos = easeOutExpo(timeElapsed, startPos, distance, duration);
window.scrollTo(0, scrollPos);
if (timeElapsed < duration) {
requestAnimationFrame(animation);
} else {
window.scrollTo(0, position);
}
}
requestAnimationFrame(animation);
}
// 遅延読み込み解除
function removeLazyLoad() {
const targets = document.querySelectorAll('[data-src]');
for (const target of targets) {
target.setAttribute('src', target.getAttribute('data-src'));
target.addEventListener('load', () => {
target.removeAttribute('data-src');
});
}
}
// ページ内のスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
link.addEventListener('click', (e) => {
const hash = e.currentTarget.hash;
const target = document.getElementById(hash.slice(1));
// ページトップへ("#"と"#top")
if (!hash || hash === '#top') {
e.preventDefault();
scrollToPos(1); // iOSのChromeで固定ヘッダーが動くバグがあるため0ではなく1に
// アンカーへ
} else if (target) {
e.preventDefault();
removeLazyLoad();
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
scrollToPos(position);
// URLにハッシュを含める
history.pushState(null, '', hash);
}
});
};
// 別ページ遷移後のスムーススクロール
const urlHash = window.location.hash;
if (urlHash) {
const target = document.getElementById(urlHash.slice(1));
if (target) {
// ページトップから開始(ブラウザ差異を考慮して併用)
history.replaceState(null, '', window.location.pathname);
window.scrollTo(0, 0);
removeLazyLoad();
window.addEventListener("load", () => {
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
scrollToPos(position);
// ハッシュを再設定
history.replaceState(null, '', window.location.pathname + urlHash);
});
}
}
こちらを実装したデモサイトの「フッターメニュー」で動きを確認できます。
- ユーザー名:demo
- パスワード:demo
固定ヘッダーの解説
// 固定ヘッダー(固定しない場合は = 0)
const headerHeight = document.querySelector('header').offsetHeight + 20;
固定ヘッダーの高さと任意の余白(px単位)をheaderHeight
に代入しています。
固定しない場合はheaderHeight = 0
にします。
イージング関数の解説
// イージング関数(easeOutExpo)
function scrollToPos(position) {
const startPos = window.scrollY;
const distance = Math.min(position - startPos, document.documentElement.scrollHeight - window.innerHeight - startPos);
const duration = 800; // スクロールにかかる時間(ミリ秒)
let startTime;
function easeOutExpo(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}
function animation(currentTime) {
if (startTime === undefined) {
startTime = currentTime;
}
const timeElapsed = currentTime - startTime;
const scrollPos = easeOutExpo(timeElapsed, startPos, distance, duration);
window.scrollTo(0, scrollPos);
if (timeElapsed < duration) {
requestAnimationFrame(animation);
} else {
window.scrollTo(0, position);
}
}
requestAnimationFrame(animation);
}
イージングには、徐々に減速する動きが最も強いイーズアウト「easeOutExpo」を採用しました。
これは、指で弾いた瞬間が最も速く、摩擦で徐々に減速するといった物理法則を再現しており、スムーススクロールに最適です。
このメリハリのある動きにより、自己帰属感(操作の感覚)が向上します。
参考にした記事
イージングを変更したい場合は、ChatGPTに頼るなどして、以下の2箇編集する必要があります。
function easeOutExpo(t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}
↓
function easeOutQuart(t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
const scrollPos = easeOutExpo( // ...
↓
const scrollPos = easeOutQuart( // ...
スクロールの最大距離を制限
const distance = Math.min(position - startPos, document.documentElement.scrollHeight - window.innerHeight - startPos);
例えば、スクロール先をフッターなどにした時、画面上部までの高さが足りず、最後までイージングアニメーションが行われないことがあります。
Math.min()
で、position
までの距離とページの残りのスクロール可能な距離の小さい方を選び、目的地がページの下端を超えるような場合でも、ページの末端を超えないようにスクロールを制限しています。
試しに、最大距離を制限しなかった時のサンプルを作ってみました。
See the Pen ページ下部のセクションが低い場合のスクロール挙動 by hisa (@hisaaashi) on CodePen.
footerの時だけ、これ以上、下にスクロールすることができないためイージングが途切れてしまうのが確認できます。
誤差によるズレを修正
if (timeElapsed < duration) {
requestAnimationFrame(animation);
} else {
window.scrollTo(0, position); // スクロール位置がずれないように修正
}
イージングの計算式の誤差で、スクロール中に数ピクセルのズレが生じてしまう場合があるため、最終位置に到達させるための再スクロール処理を追加しています。
イージング関数の使い方
デフォルトでよく使われているコードを、以下のように置き換えることでイージングを使用することができます。
// 通常のスクロール
window.scrollTo({
top: position,
behavior: "smooth",
});
↓
// イージングによるスクロール
scrollToPos(position);
遅延読み込み解除の解説
このコードは、Lazy LoadのJSライブラリを利用している際に有効ですので、JSライブラリを使っていない場合は削除しても結構です。
遅延読み込みした際に、コンテンツがうまく読み取れずスクロール先の位置がずれるという場合の対策として、強制的に読み込みを行っています。
// 遅延読み込み解除
function removeLazyLoad() {
// data-srcを変数に格納
const targets = document.querySelectorAll('[data-src]');
// 全てのdata-srcを取得
for (const target of targets) {
// srcにdata-srcの値をコピー
target.setAttribute('src', target.getAttribute('data-src'));
// ページを読み込んで処理
target.addEventListener('load', () => {
// data-srcを削除
target.removeAttribute('data-src');
});
}
}
// 実行するタイミングで以下を記述
removeLazyLoad();
LazyLoad対策の詳しい内容は、以下の記事にまとめています。
ページ内のスムーススクロールの解説
// ページ内のスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
// ハッシュを含むaタグをクリックした時の処理
link.addEventListener('click', (e) => {
// ハッシュを取得
const hash = e.currentTarget.hash;
// スクロール先を取得
const target = document.getElementById(hash.slice(1));
// ページトップへ("#"と"#top")
if (!hash || hash === '#top') {
// デフォルトの動作をキャンセル
e.preventDefault();
// スクロール開始
scrollToPos(1); // iOSのChromeで固定ヘッダーが動くバグがあるため0ではなく1に
// アンカーへ
} else if (target) {
// デフォルトの動作をキャンセル
e.preventDefault();
// 遅延読み込み解除
removeLazyLoad();
// スクロール先の位置を計算(ヘッダーの高さを引く)
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// スクロール実行
scrollToPos(position);
// URLにハッシュを含める
history.pushState(null, '', hash);
}
});
};
こちらは解説の通りシンプルです。
ただ、ページトップへのスクロールでは、最上部0pxにスクロールすると固定ヘッダーがずれるバグがあるため1px下にずらして対策しています。
別ページ遷移後のスムーススクロールの解説
// 別ページ遷移後のスムーススクロール
// ハッシュを取得
const urlHash = window.location.hash;
// ハッシュが存在する時
if (urlHash) {
// スクロール先を取得
const target = document.getElementById(urlHash.slice(1));
// スクロール先が存在する時
if (target) {
// ページトップから開始(ブラウザ差異を考慮して併用)
history.replaceState(null, '', window.location.pathname); // ハッシュを削除
window.scrollTo(0, 0); // ページトップへジャンプ
// 遅延読み込み解除
removeLazyLoad();
// ページを読み込んで処理
window.addEventListener("load", () => {
// スクロール先の位置を計算(ヘッダーの高さを引く)
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// スクロール実行
scrollToPos(position);
// ハッシュを再設定
history.replaceState(null, '', window.location.pathname + urlHash);
});
}
}
スクロールの開始位置
// ページトップから開始(ブラウザ差異を考慮して併用)
history.replaceState(null, '', window.location.pathname); // ハッシュを削除
window.scrollTo(0, 0); // ページトップへジャンプ
スクロールをページトップから開始させるための処理ですので、不要であれば削除してください。
上はURLからハッシュを削除して初期位置(トップ)へ遷移させる方法で、下は即座にページトップへジャンプさせる方法です。
ブラウザによってコードが効かない場合があったので、2種類のアプローチを併用しています。
スクロールが不要な場合
ページ遷移後にスクロールアニメーションが不要な場合は、以下のように簡略化できます。
// 別ページ遷移後、正しい位置に移動
const urlHash = window.location.hash;
if (urlHash) {
const target = document.getElementById(urlHash.slice(1));
if (target) {
removeLazyLoad();
window.addEventListener("load", () => {
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// 移動開始
window.scrollTo(0, position);
history.replaceState(null, "", window.location.pathname + urlHash);
});
}
}
まとめ:実装は全部入りで解決
以上、完全版スムーススクロールのコードのご紹介でした。
イージングや秒数など好みがあるかと思いますので、必要に応じてカスタマイズしてください。
おすすめWEBスクール
WEB制作やWEBデザインを学びたいなら、SNSでも話題の「デイトラ」がおすすめ!
どのコースも10万円前後と業界最安値で、副業や転職に向けて十分なスキルを身につけることができます。
\ クリックしてジャンプ! /
お気軽にコメントどうぞ