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

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

純粋なJavaScript(VanillaJS)で、LazyLoad(遅延読み込み)にも対応させたスムーススクロールの実装方法をご紹介します。

今回作成したスムーススクロールは、以下のような場合で有効です。

  • ページトップへスムーススクロール
  • ページ内のアンカーへスムーススクロール
  • 別ページ遷移後にアンカーへスムーススクロール
  • 固定ヘッダーの被り対策
  • Lazy Load(遅延読み込み)による位置ずれ対策

また、イージング(動き)をeaseOutExpoに指定しているので、より操作感の強い印象になっているかと思います。

WordPressのおすすめサーバー

特徴
  • 料金が安い
  • WordPressが超高速
  • ドメイン永久無料
  • 安心の実績とサポート体制

新規も乗り換えも

目次

完全版スムーススクロールのコード

まずは全体のコードを先にお見せします。

// 固定ヘッダーの高さ(+余白の追加)
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);
    });
  }
}

このコードは、以下の順で記述しています。

  1. 固定ヘッダーの高さ
  2. イージング関数
  3. LazyLoad対策の関数
  4. ページ内のスムーススクロール
    • ページトップへ
    • アンカーへ
  5. 別ページ遷移後にスムーススクロール

通常のスムーススクロールのコードと違う点は、イージング関数があることと、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下にずらして対策しています。

また、htmlbodyなどに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デザイナーとなりました。

他にも様々なコースが充実しているので、身につけたいスキルがあったら是非覗いてみてください。

コースの一部をご紹介

よかったらシェアしてね!

コメント

コメントする

目次