今回はページ内リンクの遷移でよく使う、「スムーススクロール」をjQueryに頼らず実装する方法をメモしていきます。
「スムーススクロール」とは、リンク先まですす〜っとアニメーション付きでスクロールしてくれるやつのことです。
こういったスクロールアニメーションはjQueryなしでは難しいとされている処理の一つですが、色々試行錯誤したところ、問題なく動作するようなコードが完成したので紹介しておきます。
jQueryなしでスムーススクロールさせる動作デモ
まずは実際の動作を確認していただきましょう。
See the Pen jQueryなしでページ内リンクをスムーススクロールさせるコード by ddryo (@ddryo-the-encoder) on CodePen.
問題なくスムーススクロールされていますね。
JSコード部分を見てもらえると、jQueryを使用していないことも確認していただけるかと思います。
※ ページの高さが小さい時、リンク4に移動した状態だとリンク3までがスムーススクロールされないようです。
実際に使う時にはほぼ発生しない現象かなと思うのでひとまず放置していますが、改善方法が分かる方がいましたら、コメントいただければ助かります...。
では、このJSコードについて解説いこうと思います。
スムーススクロールのコード説明
コードの全貌はCodePenを見ていただくとして、
今回使用しているコードのポイントとなる部分をざっと解説していきます。
スムーススクロールの実行部分を関数化
今回の私のコードではmスムーススクロールを実行する部分の処理を関数化しています。
smoothScroll 関数の定義部分
/**
* スムーススクロール実行関数
*/
let smoothScroll = (target, offset) => {
let toY;
let nowY = window.pageYOffset; //現在のスクロール値
const divisor = 8; //近づく割合(数値が大きいほどゆっくり近く)
const range = (divisor / 2) + 1; //どこまで近づけば処理を終了するか(無限ループにならないように divisor から算出)
//ターゲットの座標
const targetRect = target.getBoundingClientRect(); //ターゲットの座標取得
const targetY = targetRect.top + nowY - offset; //現在のスクロール値 & ヘッダーの高さを踏まえた座標
//スクロール終了まで繰り返す処理
(function () {
let thisFunc = arguments.callee; //自身を呼び出すために変数に代入
toY = nowY + Math.round((targetY - nowY) / divisor); //次に移動する場所(近く割合は除数による。)
window.scrollTo(0, toY); //スクロールさせる
nowY = toY; //nowY更新
if (document.body.clientHeight - window.innerHeight < toY) {
//最下部にスクロールしても対象まで届かない場合は下限までスクロールして強制終了
window.scrollTo(0, document.body.clientHeight);
return;
}
if (toY >= targetY + range || toY <= targetY - range) {
//+-rangeの範囲内へ近くまで繰り返す
window.setTimeout(thisFunc, 10);
} else {
//+-range の範囲内にくれば正確な値へ移動して終了。
window.scrollTo(0, targetY);
}
})();
};
※ ES2015(ES6)の記法で記述しています。
処理内容
ざっくりとこの関数の処理の流れを示しておきます。
- 第一引数で、スクロール先のターゲットを受け取る
- 現在のスクロール量
nowY
を取得 - ターゲットの絶対座標
targetRect
を取得 - 現在のスクロール量や第二引数のオフセット値をふまえ、最終的なスクロール位置
targetY
を計算 - 最終的なスクロール位置の近く(
targetY
+-range
の範囲)に到達するまで、徐々に近づいていく処理を繰り返す- 次のスクロール位置(現在の位置からターゲットまでの距離の 1/8(=
divisor
)近づいた点 の座標)を計算(toY
) - その位置(
toY
)までスクロールを実行 - この時、次のスクロール位置が一番下までスクロールしても届かない場合は、最下部までスクロールさせて処理を終了
- 10ミリ秒=(
interval
)後に同じ処理(thisFunc = arguments.callee
)を繰り返す。 - 十分近くまで近づいたら、最終的なスクロール位置(
targetY
)へスクロールさせて終了
- 次のスクロール位置(現在の位置からターゲットまでの距離の 1/8(=
といった感じです。
「徐々に近づいていく」という処理を無名関数でループさせる際、arguments.callee
でその関数自身を呼び出しています。(わかりやすいようにthisFunc
という変数に代入している)
この関数に名前をつけて、setTimeout
の中でその関数名を直接呼び出してもOKです。
自分なりにできるだけ工夫して整理はしたつもりですが、もっといいコードで実装できるかもしれません。
スクロール速度について
divisor
とinterval
がスクロールの速度に関係します。
好きな値に調節してください。
rangeについて
どこまで近づけば処理を終了するかの範囲を決めるのがrange
です。
これがなぜdivisor
を用いた式で計算されているかというと、次のスクロール位置toY
の計算時に小数点以下四捨五入する部分があり、これが0.5未満になるとそれ以上値が変化せず無限に処理が続くためです。
そのような値を取らないようにrange
を定める必要があり、それはdivisor
によって変化するので、式で計算しています。
#付きのアンカータグにクリックイベントを登録する
上記の関数を定義しただけでは、もちろん何も動作しません。
なので、ページ内リンク用のアンカータグ(href属性に#{id}
の指定されているリンク)に対し、クリックイベントを登録し、その中でスムーススクロール実行関数smoothScroll()
を呼び出してあげるようにします。
クリックイベントの登録部分
/**
* アンカータグにクリックイベントを登録
*/
const smoothOffset = headH; //Fixされているヘッダーの高さ分スクロール先の座標をずらすために、変数に高さを入れておく
const links = document.querySelectorAll('a[href*="#"]'); //#がリンクに含まれているアンカータグを全て取得
for (let i = 0; i < links.length; i++) {
links[i].addEventListener('click', function (e) {
const href = e.currentTarget.getAttribute('href'); //href取得
const splitHref = href.split('#');
const targetID = splitHref[1];
const target = document.getElementById(targetID); //リンク先の要素(ターゲット)取得
if (target) {
smoothScroll(target, smoothOffset);
} else {
return true;
}
return false;
});
}
ドメイン部分から書かれたリンク(https://example.com/#hoge
のようなリンク)も考慮して、
よくあるa[href^="#"]
ではなくa[href*="#"]
にしています。
#
以降の文字列からターゲットIDを取得し、そのターゲットが見つかれば先ほど定義しておいたsmoothScroll()
を呼び出しています。
オフセット値を渡すsmoothOffset
について
第二引数のsmoothOffset
は、そのサイトのデザインによって変化する部分です。
ヘッダーを上部に固定表示させていて、ページ内リンクの位置に移動するとヘッダーが被ってしまう場合に、そのヘッダーの高さを取得して渡すようにしてあげてください。