【完全版】スムーススクロールの実装は全てこれでいい【LazyLoad対応】

【完全版】スムーススクロールの実装は全てこれでいい【LazyLoad対応】
追記:2024年1月1日

コードを見直し細かい箇所をリファクタリングしました。主要ブラウザ全てで動作確認済みです。

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万円前後と業界最安値で、副業や転職に向けて十分なスキルを身につけることができます。

役に立ったら他の方にシェア

お気軽にコメントどうぞ

コメントする

目次