2021-10-11
BlogManagement, JavaScript
JavaScript追加のみでスムーススクロール実装する [Safari, iOS対応]
ページ内リンクで遷移する際に, 目的の箇所にパッとジャンプするのではなく, スムースにスクロールして移動させたかった.
これは, その遷移が画面内の上下どっちに行くのかを直感的にわかるようにしたかったため.
簡単実装する際には, cssで全体に対して
html { scroll-behavior: smooth;}
といったことをすれば実現できる. ただし, この手法だと, Safari, iOS上のChrome, Safariなどでは機能せず, パッとジャンプになってしまう.
そこで, JavaScriptをHeadに読み込むだけで, aタグのページ内リンク (href="#hoge"等) スムーススクロールになるようなものを作成してみた.
- aタグに特別な変更が要らずHeadでの読み込みだけなので導入が楽なこと
- jQueryを利用する手法よりも軽いこと
などがメリットに挙げられると思う. また, スクロールの滑らかさについてもある程度調整可能なようにした.
目次
動画
Macbook Air (M1 2020)
| Safari | Chrome |
|---|---|
![]() | ![]() |
iPhone 8
| Safari | Chrome |
|---|---|
![]() | ![]() |
実装
上記のテストページのサンプルコードを掲載する. 以下の3ファイルを同一ディレクトリに入れ, index.htmlを開けば動作する. インストール等は不要.
.├── index.html├── smooth-scroll.js└── stylesheet.css
index.html
<html lang="ja">
<head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width initial-scale=1'> <link rel="stylesheet" href="stylesheet.css"> <script defer type="text/javascript" src="smooth-scroll.js"></script> <title>scrolle test</title></head><body> <aside class="indexWrapper"> <h1>Index</h1> <ol class="index"> <li><a href="#index01">Go to 1</a></li> <li><a href="#index02">Go to 2</a></li> <li><a href="#index03">Go to 3</a></li> <li><a href="#index04">Go to 4</a></li> <li><a href="#index05">Go to 5</a></li> </ol> </aside> <main class="contents"> <p>0000000aaaaaaa</p> : : <p>0000000aaaaaaa</p> <div class="box" id="index01"> <h2>1</h2> <p>1111111aaaaaaa</p> : : <p>1111111aaaaaaa</p> </div> <div class="box" id="index02"> <h2>2</h2> <p>222222aaaaaaaa</p> : : <p>222222aaaaaaaa</p> </div> <div class="box" id="index03"> <h2>3</h2> <p>333333aaaaaaaa</p> : : <p>333333aaaaaaaa</p> </div> <div class="box" id="index04"> <h2>4</h2> <p>444444aaaaaaaa</p> : : <p>444444aaaaaaaa</p> </div> <div class="box" id="index05"> <h2>5</h2> <p>555555aaaaaaaa</p> : : <p>555555aaaaaaaa</p> </div> </main></body></html>
<script defer type="text/javascript" src="smooth-scroll.js"></script>のように, jsの読み込み指定をdeferにしている. asyncで実装していた時期もあったが, その際, たまにjsがうまく動作しないことがあった (jsが先に実行されてしまっていた?).
stylesheet.css
/* html { scroll-behavior: smooth;} */
.indexWrapper { position: fixed; right: 50px;}
scroll-behavior: smooth はあってもなくても良い.
実際のスクロール時にのみ無効化するようにしているため
.indexWrapperの部分も目次を右側に固定表示するためのものなので, 今回の実装の本質ではない.
smooth-scroll.js
const interval_ms = 10;const move_division = 10.0;const y_shift = -50;const y_threhold = 1;var y_diff;var y;var y_target;var count;var selectedTarget;var previousTarget;var isMoving = false;
const aTags = document.querySelectorAll("a");aTags.forEach(aTag => { aTag.onclick = function() { document.querySelector("html").style.scrollBehavior = "auto"; var hrefTarget = aTag.getAttribute("href"); if (hrefTarget != null) { var firstCharactor = hrefTarget.substr(0,1); if(firstCharactor == "#"){ var targetName = hrefTarget.substr(1); // console.log(previousTarget == targetName); if ((previousTarget == targetName && isMoving == false) || (previousTarget != targetName)){ isMoving = true; selectedTarget = targetName; aTag.removeAttribute("href"); count = 0; console.log(" click:", targetName); scroll_start(targetName, aTag); previousTarget = targetName; } else { console.log("moving:", targetName); aTag.removeAttribute("href"); } } } }});
function scroll(p_target, aTag) { if (p_target == selectedTarget){ // console.log("OK:", p_target, selectedTarget); count += 1; var y_move = y_diff / move_division; y += y_move; scrollTo(0, y); y_diff = y_target - y; if ((y_diff < 0 && y_target < y) || (0 < y_diff && y < y_target)) { if (Math.abs(y_diff) >= y_threhold) { setTimeout(function(){scroll(p_target, aTag);}, interval_ms); // console.log("[1-1]", p_target, aTag, count); } else { scrollTo(0, y_target); document.querySelector("html").style.scrollBehavior = "smooth"; // window.location.href = "#"+p_target; isMoving = false; // console.log("[1-2]", p_target, aTag, count, isMoving); console.log(" end:", p_target); } if (count >= 2) { // console.log("Tag is backed [1]"); aTag.setAttribute("href", "#"+p_target); } } else { if (count == 1) { setTimeout(function(){scroll(p_target, aTag);}, interval_ms); // console.log("[2-1]", p_target, aTag, count); } else { // console.log("Tag is backed [2]"); aTag.setAttribute("href", "#"+p_target); document.querySelector("html").style.scrollBehavior = "smooth"; // window.location.href = "#"+p_target; isMoving = false; // console.log("[2-2]", p_target, aTag, count, isMoving); console.log(" end:", p_target); } } } else { // console.log("NG:", p_target, selectedTarget); // console.log("Tag is backed [3]"); aTag.setAttribute("href", "#"+p_target); console.log("cancel:", p_target); }}
function scroll_start(p_target, aTag) { // scroll position from top of a page to top of a current view area y = window.pageYOffset; // scroll position from top of a current view area to top of a target element y_diff = parseInt(document.getElementById(p_target).getBoundingClientRect().top); // calculate top position of target y_target = y_diff + y + y_shift;
scroll(p_target, aTag);}
実装内容としては, 現在位置から目的の箇所まで, 細かいジャンプを繰り返して到達している. その細かさがある程度細かいことにより, スムースにスクロールしているように見える.
以下が調整パラメータ:
const interval_ms = 10;- 細かいジャンプをする時間間隔 [ミリ秒]
const move_division = 10.0;細かいジャンプの移動割合
現在位置から目的の箇所までの距離をYとした場合
- 10なら,
間隔ずつ進んでいく. - 2なら,
間隔ずつ進んでいく.
- 10なら,
const y_shift = -50;- スクロールした際の上下のシフト量 [px]
- 負の場合, 本来の目的の箇所より下に到達する
- 正の場合, 本来の目的の箇所より上に到達する
const y_threhold = 1;- 目的の箇所付近でスクロールを終える終了条件として閾値 [px]
- スクロールしていき, 目的の箇所との誤差がこの閾値px以下になったら目的箇所にジャンプし, スクロールを終了する.
基本的には, そのままで良いと思うが, 必要に応じて, interval_ms, move_division, y_shift あたりを調整すると良い.
y_thresholdは調整する必要はほとんどないと思われる.
参照
主な参照元
- JavaScriptで簡単にスムーズスクロール ( リモートデスクトップエンジニアのブログ。 )
- JavaScript 属性値を取得/設定/削除する(Attribute) | ITSakura
- スクリプトの非同期読み込み(async, deferの違い)
その他
- JavaScriptで簡単にスムーズスクロール ( リモートデスクトップエンジニアのブログ。 )
- 右シフト (>>) - JavaScript | MDN
- シフト演算
- Math.abs() - JavaScript | MDN
- scroll-behavior - CSS: カスケーディングスタイルシート | MDN
- 【JavaScript】スクロール量を取得する【pageXOffset、pageYOffset】|Into the Program
- スクロール位置取得方法をいい加減忘れないようにメモ - Qiita
- HTML で aタグを使ってるのに、矢印カーソルが出てこなくてなんでやねんと思った時 - Qiita
- 【初心者向け】あえてJavascriptを使ってリンク先に飛ぶ - ジャングルオーシャン
- JavaScriptでURLを遷移させる方法 - Qiita
- BeautifulSoupを用いたHTMLデータの検索方法 - Qiita
- jQueryでスムーススクロールを実装する方法 | TechAcademyマガジン
- 【デモ】jQuery:「smooth-scroll.js」を使ってページ内リンククリック時の滑らかなスクロールを実装する:電脳職人村
- 【初心者向け】jQueryとは|メリット・デメリットから記述方法まで解説 | パソナテック
- クリック時の処理!JavaScriptでonclickを使う方法 | TechAcademyマガジン
- getElementsByTagName() タグ名から要素を取得 | JavaScript中級編 - ウェブプログラミングポータル
- JavaScript 属性値を取得/設定/削除する(Attribute) | ITSakura
- クリック時の処理!JavaScriptでonclickを使う方法 | TechAcademyマガジン
- document.getelementsbytagname onclick - Google 検索
- 【JavaScript入門】文字列の分割と切り出し(substr/substring/slice) | 侍エンジニアブログ
- 【JavaScriptの条件分岐】if文の書き方/使い方 - サンプルコードとあわせて解説 | パソナテック
- JavaScriptでスタイルの動的変更方法 - Qiita
- JavaScriptのコードから要素のスタイルを変更する : JavaScript | iPentec
- scroll-behavior - CSS: カスケーディングスタイルシート | MDN
- setInterval()やsetTimeout()に引数ありの関数を指定する | ソフトウェア開発日記
- .setAttribute() | JavaScript 日本語リファレンス | js STUDIO
- JavaScriptでURLを遷移させる方法 - Qiita



