ページ内リンクでスムーズスクロールする - スクロールアニメーションするリンクの作成 - JavaScript

JavaScriptを利用して、スクロールアニメーション(スムーズスクロール)するページ内リンクを作成します。
メモ
新しいブラウザでは、CSSのscroll-behaviorプロパティの設定で実現できます。 詳細はこちらの記事を参照してください。

概要

ページ内のリンクはアンカータグを記述して遷移先にページ内のアンカーポイントを設定することで実現できますが、リンククリック時に画面が完全に切り替わる動作になります。この記事ではページ内のリンクをクリックして遷移したときにアンカーポイントの位置まで画面をスムーズにスクロールして切り替えるコードを紹介します。
メモ
jQueryを利用する実装はこちらの記事を参照してください。

実直な実装方法

動作をわかりやすくするため単純な実装のコードを紹介します。

コード

AnchorScrollNative.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link rel="stylesheet" type="text/css" href="AnchorScrollNative.css" />
    <script type="text/javascript">
      window.onload = function () {
        document.getElementById('link1').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link2').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link3').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link4').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link5').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link6').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }

      };

      function getElementAbsoluteTop(id) {
        var target = document.getElementById(id);
        var rect = target.getBoundingClientRect();
        return rect.top;
      }

      function scrollScreen(desty, time) {
        var top = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
        var tick = desty / time;

        if (top < desty) {
          var newy = top + tick;
          document.documentElement.scrollTop = newy;
          setTimeout("scrollScreenDown(" + top + "," + desty + "," + newy + "," + tick + ")", 20);

        } else {
          var newy = top + tick;
          document.documentElement.scrollTop = newy;
          setTimeout("scrollScreenUp(" + top + "," + desty + "," + newy + "," + tick + ")", 20);
        }
      }

      function scrollScreenDown(starty, desty, newy, tick) {
        if (newy < starty + desty) {
          var newy = newy + tick;
          if (starty + desty < newy) newy = starty + desty;

          document.documentElement.scrollTop = newy;
          setTimeout("scrollScreenDown(" + starty + "," + +desty + "," + newy + "," + tick + ")", 20);
        }
      }

      function scrollScreenUp(starty, desty, newy, tick) {
        if (starty + desty < newy) {
          var newy = newy + tick;
          if (starty + desty > newy) newy = starty + desty;

          document.documentElement.scrollTop = newy;
          setTimeout("scrollScreenUp(" + starty + "," + desty + "," + newy + "," + tick + ")", 20);
        }
      }
    </script>
</head>
<body>
  <a id="link1" href="#section1">セクション1へ</a><br />
  <a id="link2" href="#section2">セクション2へ</a><br />
  <a id="link3" href="#section3">セクション3へ</a><br />
  <hr />

  <a id="section1">セクション1</a>
  <div style="height:300px">コンテンツ</div> 

  <a id="section2">セクション2</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="section3">セクション3</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="link4" href="#section1">セクション1へ</a><br />
  <a id="link5" href="#section2">セクション2へ</a><br />
  <a id="link6" href="#section3">セクション3へ</a><br />
  <hr />
</body>
</html>

解説

window.onload 部
ウィンドウが表示された際にページ内リンクのidを指定してJavaScriptのClickイベントを割り当てます。

document.getElementById('link1').onclick = function () {
  var top = getElementAbsoluteTop('section1');
  scrollScreen(top, 20);
  return false;
}
Clickイベントとして割り当てた関数が上記になります。getElementAbsoluteTop()関数を呼び出し、与えたIDの要素の縦方向の絶対位置を求めます。求めた絶対位置をscrollScreen()関数に与えてスクロールします。第二引数は何分割してアニメーションするか(フレーム数)を指定します。
getElementAbsoluteTop 関数
  function getElementAbsoluteTop(id) {
    var target = document.getElementById(id);
    var rect = target.getBoundingClientRect();
    return rect.top;
  }
与えたIDの要素の上端の絶対位置を求める関数です。処理内容は、document.getElementById()メソッドで与えたIDの要素を取得し、getBoundingClientRect()メソッドで要素を囲む矩形の座標を取得しそのtopの値を戻り値として戻します。
scrollScreen関数
function scrollScreen(desty, time) {
  var top = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
  var tick = desty / time;

  if (top < desty) {
    var newy = top + tick;
    document.documentElement.scrollTop = newy;
    setTimeout("scrollScreenDown(" + top + "," + desty + "," + newy + "," + tick + ")", 20);
  } else {
    var newy = top + tick;
    document.documentElement.scrollTop = newy;
    setTimeout("scrollScreenUp(" + top + "," + desty + "," + newy + "," + tick + ")", 20);
  }
}
スクロール開始のための関数です。スクロールさせる量をフレーム数で割って1回あたりのスクロール量(tick)を求めます。現在のスクロール位置からtick分だけスクロールさせます。その後setTimeout関数を呼び出し20ms後に次の関数を呼び出します。上に向かってスクロールする場合はscrollScreenUp()関数、下に向かってスクロールする場合はscrollScreenDown()関数を呼び出します。
scrollScreenDown 関数
function scrollScreenDown(starty, desty, newy, tick) {
  if (newy < starty + desty) {
    var newy = newy + tick;
    if (starty + desty < newy) newy = starty + desty;

    document.documentElement.scrollTop = newy;
    setTimeout(function(){scrollScreenDown(starty, desty, newy, tick);}, 20);
  }
}
下方向に向かってスクロールする関数です。現在のスクロール位置からtick文スクロールします。その後、setTimeout関数を呼び出し20ms後にs同じ(crollScreenDown())関数を呼び出します。スクロールの位置が遷移先の位置になった場合はsetTimeout関数の呼び出しをせずにスクロールアニメーションを終了します。
scrollScreenUp 関数
function scrollScreenUp(starty, desty, newy, tick) {
  if (starty + desty < newy) {
    var newy = newy + tick;
    if (starty + desty > newy) newy = starty + desty;

    document.documentElement.scrollTop = newy;
    setTimeout(function(){scrollScreenUp(starty, desty, newy, tick);}, 20);
  }
}
上方向に向かってスクロールする関数です。処理内容はcrollScreenDown()関数と同じです。
HTML部
<body>
  <a id="link1" href="#section1">セクション1へ</a><br />
  <a id="link2" href="#section2">セクション2へ</a><br />
  <a id="link3" href="#section3">セクション3へ</a><br />
  <hr />

  <a id="section1">セクション1</a>
  <div style="height:300px">コンテンツ</div> 

  <a id="section2">セクション2</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="section3">セクション3</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="link4" href="#section1">セクション1へ</a><br />
  <a id="link5" href="#section2">セクション2へ</a><br />
  <a id="link6" href="#section3">セクション3へ</a><br />
  <hr />
</body>
HTML部分のコードです。ページ内へのリンクを設置するコードと同じです。リンク元のaタグにIDをつける必要があることに注意してください。

setTimeoutを整理した実装

先の実装でも問題なく動作しますが、setTimeoutの呼び出し周りのコードがきれいでないので、setTimeoutの呼び出しを整理したコードを紹介します。上記のコードのJavaScript 部分を下記のコードに置き換えます。
    <script type="text/javascript">
      window.onload = function () {
        document.getElementById('link1').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link2').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link3').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link4').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link5').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link6').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }

      };

      function getElementAbsoluteTop(id) {
        var target = document.getElementById(id);
        var rect = target.getBoundingClientRect();
        return rect.top;
      }

      function scrollScreen(desty, time) {
        var top = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
        var tick = desty / time;

        if (top < desty) {
          var newy = top + tick;
          document.documentElement.scrollTop = newy;
          setTimeout(function(){scrollScreenDown(top, desty, newy, tick);}, 20);

        } else {
          var newy = top + tick;
          document.documentElement.scrollTop = newy;
          setTimeout(function(){scrollScreenUp(top, desty, newy, tick);}, 20);
        }
      }

      function scrollScreenDown(starty, desty, newy, tick) {
        if (newy < starty + desty) {
          var newy = newy + tick;
          if (starty + desty < newy) newy = starty + desty;

          document.documentElement.scrollTop = newy;
          setTimeout(function(){scrollScreenDown(starty, desty, newy, tick);}, 20);
        }
      }

      function scrollScreenUp(starty, desty, newy, tick) {
        if (starty + desty < newy) {
          var newy = newy + tick;
          if (starty + desty > newy) newy = starty + desty;

          document.documentElement.scrollTop = newy;
          setTimeout(function(){scrollScreenUp(starty, desty, newy, tick);}, 20);
        }
      }
    </script>

解説

先のコードでは、setTimeoutの第一引数は文字列で関数呼び出しのコードを与えています。この方法でも動作しますが、「"」が増えてしまいコードの可読性が落ちてしまいます。また、文字列を渡すため動作にも若干の不安があります。
  setTimeout("scrollScreenUp(" + top + "," + desty + "," + newy + "," + tick + ")", 20);

整理後のコードでは関数オブジェクトを第一引数に渡します。「"」がなくなるためコードも見やすくなります。
  setTimeout(function(){scrollScreenUp(starty, desty, newy, tick);}, 20);

上方向へのスクロールと下方向へのスクロールを共通化

上方向へのスクロールと下方向へのスクロールの処理がほとんど同じためコードを共通にしてまとめたコードが下記になります。

コード

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link rel="stylesheet" type="text/css" href="AnchorScrollNative.css" />
    <script type="text/javascript">
      window.onload = function () {
        document.getElementById('link1').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link2').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link3').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link4').onclick = function () {
          var top = getElementAbsoluteTop('section1');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link5').onclick = function () {
          var top = getElementAbsoluteTop('section2');
          scrollScreen(top, 20);
          return false;
        }
        document.getElementById('link6').onclick = function () {
          var top = getElementAbsoluteTop('section3');
          scrollScreen(top, 20);
          return false;
        }

      };

      function getElementAbsoluteTop(id) {
        var target = document.getElementById(id);
        var rect = target.getBoundingClientRect();
        return rect.top;
      }

      function scrollScreen(desty, time) {
        var top = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
        var tick = desty / time;

        var newy = top + tick;
        document.documentElement.scrollTop = newy;
        setTimeout(function () { scrollScreenInt(top, desty, newy, tick); }, 20);
      }

      function scrollScreenInt(starty, desty, newy, tick) {
        var stop=true;

        var newy = newy + tick;
        if (desty < 0) {
          if (starty + desty < newy) {
            stop = false;
          } else {
            newy = starty + desty;
          }
        } else {
          if (newy < starty + desty) {
            stop = false;
          } else {
            newy = starty + desty;
          }
        }

        document.documentElement.scrollTop = newy;
        if (stop == false) {
          setTimeout(function () { scrollScreenInt(starty, desty, newy, tick); }, 20);
        }
      }
    </script>
</head>
<body>
  <a id="link1" href="#section1">セクション1へ</a><br />
  <a id="link2" href="#section2">セクション2へ</a><br />
  <a id="link3" href="#section3">セクション3へ</a><br />
  <hr />

  <a id="section1">セクション1</a>
  <div style="height:300px">コンテンツ</div> 

  <a id="section2">セクション2</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="section3">セクション3</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="link4" href="#section1">セクション1へ</a><br />
  <a id="link5" href="#section2">セクション2へ</a><br />
  <a id="link6" href="#section3">セクション3へ</a><br />
  <hr />
</body>
</html>

idを引数で渡す

先のコードでは、リンクのclickイベントはリンクごとに別々の関数を準備しましたが、リンクの個数が増えるとコード量も増え管理が大変になります。リンクのクリックイベントを共通の関数にして、クリックされたリンクのID、または遷移先の要素のIDを引数として渡す構造にすることで、リンクのclickイベントを一つの関数で実装できます。
clickイベントを共通にして、遷移先のリンクのIDを引数で渡したコードが下記になります。

コード

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <link rel="stylesheet" type="text/css" href="AnchorScrollNative.css" />
    <script type="text/javascript">
      function LinkClick(id) {
        var top = getElementAbsoluteTop(id);
        scrollScreen(top, 20);
        return false;
      }

      function getElementAbsoluteTop(id) {
        var target = document.getElementById(id);
        var rect = target.getBoundingClientRect();
        return rect.top;
      }

      function scrollScreen(desty, time) {
        var top = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
        var tick = desty / time;

        var newy = top + tick;
        document.documentElement.scrollTop = newy;
        setTimeout(function () { scrollScreenInt(top, desty, newy, tick); }, 20);
      }

      function scrollScreenInt(starty, desty, newy, tick) {
        var stop=true;

        var newy = newy + tick;
        if (desty < 0) {
          if (starty + desty < newy) {
            stop = false;
          } else {
            newy = starty + desty;
          }
        } else {
          if (newy < starty + desty) {
            stop = false;
          } else {
            newy = starty + desty;
          }
        }

        document.documentElement.scrollTop = newy;
        if (stop == false) {
          setTimeout(function () { scrollScreenInt(starty, desty, newy, tick); }, 20);
        }

      }
    </script>
</head>
<body>
  <a href="#section1" onclick="return LinkClick('section1');">セクション1へ</a><br />
  <a href="#section2" onclick="return LinkClick('section2');">セクション2へ</a><br />
  <a href="#section3" onclick="return LinkClick('section3');">セクション3へ</a><br />
  <hr />

  <a id="section1">セクション1</a>
  <div style="height:300px">コンテンツ</div> 

  <a id="section2">セクション2</a> 
  <div style="height:300px">コンテンツ</div> 

  <a id="section3">セクション3</a> 
  <div style="height:300px">コンテンツ</div> 

  <a href="#section1" onclick="return LinkClick('section1');">セクション1へ</a><br />
  <a href="#section2" onclick="return LinkClick('section2');">セクション2へ</a><br />
  <a href="#section3" onclick="return LinkClick('section3');">セクション3へ</a><br />

  <!-- こちらの記述方式でもOK -->
  <!--
  <a href="#section1" onclick="LinkClick('section1');return false;">セクション1へ</a><br />
  <a href="#section2" onclick="LinkClick('section2');return false;">セクション2へ</a><br />
  <a href="#section3" onclick="LinkClick('section3');return false;">セクション3へ</a><br />
  -->
  <hr />
</body>
</html>

実行結果

上記のHTMLファイルをWebブラウザで表示します。下図の画面が表示されます。


[セクション3へ]のリンクをクリックします。スクロールのアニメーションをしながら、セクション3までスクロールで表示が切り替わります。


[セクション2へ]のリンクをクリックします。同様にセクション2までスクロールで表示が切り替わります。


このページのキーワード
  • アンカーポイント
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2024-06-25
改訂日: 2023-05-01
作成日: 2013-06-28
iPentec all rights reserverd.