純粋なJavaScript(VanillaJS)で、LazyLoad(遅延読み込み)にも対応させたスムーススクロールの実装方法をご紹介します。
今回作成したスムーススクロールは、以下のような場合で有効です。
- ページトップへスムーススクロール
- ページ内のアンカーへスムーススクロール
- 別ページ遷移後にアンカーへスムーススクロール
- 固定ヘッダーの被り対策
- Lazy Load(遅延読み込み)による位置ずれ対策
また、イージング(動き)をeaseOutExpoに指定しているので、より操作感の強い印象になっているかと思います。
完全版スムーススクロールのコード
まずは全体のコードを先にお見せします。
// 固定ヘッダーの高さ(+余白の追加)
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);
}
// LazyLoad対策(遅延読み込み解除)
function removeLazyLoad() {
const targets = document.querySelectorAll('[data-src]');
for (const target of targets) {
target.addEventListener('load', () => {
target.removeAttribute('data-src');
});
target.setAttribute('src', target.getAttribute('data-src'));
}
}
// ページ内リンクのスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
link.addEventListener('click', (e) => {
const hash = e.currentTarget.hash;
// "#"と"#top"の時(ページトップへスクロール)
if (!hash || hash === '#top') {
e.preventDefault();
scrollToPos(1); // iOSのChromeでfixedされた固定ヘッダーなどが動くバグがあるため0ではなく1に
// それ以外の時(アンカーへスクロール)
} else {
const target = document.getElementById(hash.slice(1));
if (target) {
e.preventDefault();
// 遅延読み込み解除
removeLazyLoad();
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
scrollToPos(position);
// URLをアンカーで更新する
history.pushState(null, null, hash);
}
}
});
};
// 別ページ遷移後にスムーススクロール
const hash = window.location.hash;
if (hash) {
const target = document.getElementById(hash.slice(1));
if (target) {
// 遅延読み込み解除
removeLazyLoad();
window.addEventListener("load", () => {
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo(0, 0);
scrollToPos(position);
});
}
}
関数を含んでいるため少し長く感じますが、順に読み取るとシンプルです。
※詳しい解説は後ほど
こちらを実装したデモサイトをリンクしておきます。
- ユーザー名:demo
- パスワード:demo
スムーススクロールの基本形
上記コードのベースとなる、シンプルなスムーススクロールのコードを掲載しておきます。
こちらは、イージング関数とLazy Load対策を外した基本の形です。
// 固定ヘッダーの高さ(+余白の追加)
const headerHeight = document.querySelector('header').offsetHeight + 20;
// ページ内リンクのスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
link.addEventListener('click', (e) => {
const hash = e.currentTarget.hash;
// "#"と"#top"の時(ページトップへスクロール)
if (!hash || hash === '#top') {
e.preventDefault();
window.scrollTo({
top: 1, // iOSのChromeでfixedされた固定ヘッダーなどが動くバグがあるため0ではなく1に
behavior: 'smooth',
});
// それ以外の時(アンカーへスクロール)
} else {
const target = document.getElementById(hash.slice(1));
if (target) {
e.preventDefault();
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo({
top: position,
behavior: "smooth",
});
// URLをアンカーで更新する
history.pushState(null, null, hash);
}
}
});
};
必要最低限で良い場合は、こちらのコードで実装してもOKです。
別のページに飛んだ後、アンカーにスクロールさせたい場合は以下のコードも追加します。
// 別ページ遷移後にスムーススクロール
const hash = window.location.hash;
if (hash) {
const target = document.getElementById(hash.slice(1));
if (target) {
window.addEventListener("load", () => {
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
window.scrollTo(0, 0);
window.scrollTo({
top: position,
behavior: "smooth",
});
});
}
}
また固定ヘッダーがなければ、headerHeightの値を0にするか、headerHeightが記述されている箇所を削除すればOKです。
スムーススクロールのコードの解説
解説付き完全版スムーススクロールのコード
こちらは解説つきの全体コードです。
// 固定ヘッダーの高さ(+余白の追加)
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);
}
// LazyLoad対策(遅延読み込み解除)
function removeLazyLoad() {
// data-srcを変数に格納
const targets = document.querySelectorAll('[data-src]');
// 全てのdata-srcを取得
for (const target of targets) {
// ページを読み込んで処理
target.addEventListener('load', () => {
// data-srcを削除
target.removeAttribute('data-src');
});
// srcにdata-srcの値をコピー
target.setAttribute('src', target.getAttribute('data-src'));
}
}
// ページ内リンクのスムーススクロール
for (const link of document.querySelectorAll('a[href*="#"]')) {
// ハッシュを含むaタグをクリックした時の処理
link.addEventListener('click', (e) => {
const hash = e.currentTarget.hash;
// "#"と"#top"の時(ページトップへスクロール)
if (!hash || hash === '#top') {
// デフォルトの動作をキャンセル
e.preventDefault();
// スクロール開始
scrollToPos(1); // iOSのChromeでfixedされた固定ヘッダーなどが動くバグがあるため0ではなく1に
// それ以外の時(アンカーへスクロール)
} else {
const target = document.getElementById(hash.slice(1));
// スクロール先がある場合
if (target) {
// デフォルトの動作をキャンセル
e.preventDefault();
// 遅延読み込み解除
removeLazyLoad();
// スクロール先の距離を取得
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// スクロール開始
scrollToPos(position);
// URLをアンカーで更新する
history.pushState(null, null, hash);
}
}
});
};
// 別ページ遷移後にスムーススクロール
const hash = window.location.hash;
// URLにアンカーがある場合
if (hash) {
const target = document.getElementById(hash.slice(1));
// スクロール先がある場合
if (target) {
// 遅延読み込み解除
removeLazyLoad();
// ページを読み込んで処理
window.addEventListener("load", () => {
// スクロール先の距離を取得
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// スクロールのスタート地点(x, y)
window.scrollTo(0, 0);
// スクロール開始
scrollToPos(position);
});
}
}
このコードは、以下の順で記述しています。
- 固定ヘッダーの高さ
- イージング関数
- LazyLoad対策の関数
- ページ内のスムーススクロール
- ページトップへ
- アンカーへ
- 別ページ遷移後にスムーススクロール
通常のスムーススクロールのコードと違う点は、イージング関数があることと、LazyLoad対策の関数があることです。
コードの全てを解説するととても長くなるので要点のみ解説します。
イージング関数の解説
イージング関数は、任意の距離までeaseOutExpo
の動きになるようにChatGPTに聞きながら作成しました。
関数は計算式なのでどうしても複雑になりますが、中身を理解する必要はありません。
easeOutExpoとは?
easeOutExpo
は、イーズアウト( 徐々に減速する)が最も強い動きです。
イーズアウトは何か指で弾いた時など、弾かれた瞬間が一番速く、摩擦で徐々に減速するといった物理法則に則った動きになります。
このイージングを使用することで、動きにメリハリがつき、自己帰属感(操作の感覚)が増します。

別のイージングに変更したければ、関数(計算式)を以下のように変更します。
※ChatGPTに聞けば、計算式を出してくれます。
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( // ...
例としてeaseOutQuart
に変更してみました。

コメントアウトの解説
// スクロールの最大距離を制限
const distance = Math.min(position - startPos, document.documentElement.scrollHeight - window.innerHeight - startPos);
スクロールの最大距離をページの一番下までに制限しています。
理由は、ページ最下部にあるターゲット要素の高さがウインドウより低い場合、ページ下にぶつかってアニメーションがストップしてしまい不自然に終わってしまうためです。
if (timeElapsed < duration) {
requestAnimationFrame(animation);
} else {
window.scrollTo(0, position); // スクロール位置がずれないように修正
}
距離によっては、数ピクセル誤差によってズレが生じてしまう場合があるので、ピッタリと指定位置にスクロールするよう修正しています。
イージング関数の使い方
デフォルトでよく使われているコードを、以下のように置き換えることで使用することができます。
window.scrollTo({
top: position,
behavior: "smooth",
});
↓
scrollToPos(position);
これによって、イージング関数の式が適用されます。
Lazy Load対策の解説
遅延読み込みした際に、コンテンツがうまく読み取れずスクロール先の位置がずれるという場合の対策として、強制読み込みするコードを関数化しています。
// LazyLoad対策(遅延読み込み解除)
function removeLazyLoad() {
// data-srcを変数に格納
const targets = document.querySelectorAll('[data-src]');
// 全てのdata-srcを取得
for (const target of targets) {
// ページを読み込んで処理
target.addEventListener('load', () => {
// data-srcを削除
target.removeAttribute('data-src');
});
// srcにdata-srcの値をコピー
target.setAttribute('src', target.getAttribute('data-src'));
}
}
使用する際は画像を読み込みたいタイミングでremoveLazyLoad();
と記述すればOK。
LazyLoad対策の詳しい内容は、以下の記事にまとめています。

ページトップへスクロールの解説
if (!hash || hash === '#top') {
e.preventDefault();
scrollToPos(1); // iOSのChromeでfixedされた固定ヘッダーなどが動くバグがあるため0ではなく1に
こちらはhref="#"
とhref="#top"
の場合にページトップへスクロールするようにしています。
コメントしているように、最上部0pxにスクロールすると固定ヘッダーがずれるバグがあるため1px下にずらして対策しています。
また、html
やbody
などにidを指定してもこちらのコードが優先され、1px下にスクロールされます。

別ページ遷移後にスムーススクロールの解説
// 別ページ遷移後にスムーススクロール
const hash = window.location.hash;
// URLにアンカーがある場合
if (hash) {
const target = document.getElementById(hash.slice(1));
// スクロール先がある場合
if (target) {
// 遅延読み込み解除
removeLazyLoad();
// ページを読み込んで処理
window.addEventListener("load", () => {
// スクロール先の距離を取得
const position = target.getBoundingClientRect().top + window.scrollY - headerHeight;
// スクロールのスタート地点(x, y)
window.scrollTo(0, 0);
// スクロール開始
scrollToPos(position);
});
}
}
基本的にはコメントに書いてある通り。
どこからスクロールさせるかは、window.scrollTo(x, y)の値をスクロールのスタート位置にするか決めることができます。
ページを読み込んで処理を最初にした場合、ページの読み込み完了時に常に処理が実行されるため、余分なリソースを消費する可能性があります。
パフォーマンスを下げないためにも、ハッシュが存在する場合にのみイベントリスナーを登録し、不要な処理を避けるようにしています。
また、遅延読み込み解除もページ読み込みが完了する前に実行したいので、loadの外側に記述する必要があります。
まとめ:実装は全部入りで解決
以上、完全版スムーススクロールのコードのご紹介でした。
実質全部入りなので、あとは必要に応じて削除、カスタマイズするだけです。
出来るだけパフォーマンスが悪くならないようリファクタリングを行っていますが、もっと改良できる点があれば掲載します。
良かったらブックマークして参考にしてみてください。
カスタマイズを自分でしてみませんか?
「あの機能追加したいな」「もう少しここを調整したいな」と思ったらWeb制作のスクール「デイトラ」がおすすめです…!
カリキュラムは3ヶ月分ですが、受講生は卒業後もずっと見放題!常に最新のコンテンツに更新されるため、情報が古くなるなんてこともありません。
価格はどのコースも10万円前後と他のスクールに比べても格安です。
なのに副業・転職に十分なスキルが身につきます。

私はWeb制作とWebデザインを受講し、現在フリーランスWebデザイナーとなりました。
他にも様々なコースが充実しているので、身につけたいスキルがあったら是非覗いてみてください。
コースの一部をご紹介
コメント