【更新日 : 】
【脱Jquery】JavaScriptでスムーススクロールを実装する
- Category:
- JavaScript
- Tags:
- JavaScript, requestAnimationFrame, Vanilla JS, アニメーション, スクロール, レイアウト, 脱Jquery, 自作関数
JavaScriptでrequestAnimationFrameを用いたスムーススクロールを実装したサンプルです。
イージングのカスタマイズにも対応しています。
2024.08.07 更新
視差効果を減らす設定が入っていた際に正しくページ移動しなかった問題を修正。
2024.04.16 更新
※今回追加した機能は「スムーススクロールの実装例 | TAKLOG」を参考にさせていただきました。
(より柔軟に多機能な実装を目指す場合は、上記参考サイト様の実装例を参照することをオススメします。)
- 遷移先にフォーカスを移動させる機能を追加。
- アニメーション(視差効果)を減らす設定が有効な場合はアニメーションを切る機能を追加。
2024.02.15 更新
- クリックしたアンカーリンクをURLの履歴に追加する機能を追加。
2024.01.19 更新
- 遷移先が存在しない場合に起こる不具合の修正。
- scroll-margin-top、scroll-padding-topによる位置変更に対応。
- イージング設定をjQueryベースのものからよりメジャーな「https://easings.net/ja」に変更。
href="#top"
かhref="#"
の場合、ページの最上部へ戻ることができるHTML標準の仕様に対応。
See the Pen Smooth Scroll JavaScript (Easing support) by web_nak (@web_walking_nak) on CodePen.
HTML
HTMLの仕様通りにアンカーリンクを設定してください。
<p><a href="#img">画像まで移動</a></p>
<p id="img"><img src="https://picsum.photos/id/1003/500/600" alt="" width="500" height="600"></p>
JavaScript
イージング設定やスクロール速度はサイトに合わせて編集してください。
//スムーススクロール関数
const smoothScroll = () => {
//スクロールリンク取得
const targets = document.querySelectorAll('a[href^="#"]');
if(targets.length === 0) {
return;
}
//スクロール速度
const ANIME_SPEED = 600;
//スクロール移動の実行中にbodyへ付与するclass
const SCROLL_BUSY_CLASS = 'is-scroll-busy';
const bodyClassList = document.body.classList;
/*
スクロールアニメーションイージング設定
https://easings.net/ja
*/
const easing = (x) => {
//easeInOutQuad
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
}
//スクロール停止要素取得
const getScrollStopTarget = (target) => {
//hrefから遷移先を取得
const href = target.getAttribute('href');
let scrollStopTarget = null;
let selector = href;
//#top(HTMLの仕様でページの最上部へ戻るアンカーリンク)が手動設定されているかチェック
//https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier
if(href === '#top') {
scrollStopTarget = document.querySelector(href);
}
//#topが手動設定されていない場合はhrefから遷移先を取得
if(!scrollStopTarget) {
//#や#topの場合はページの最上部へ戻るbodyを遷移先に設定
selector = (href === '#' || href === '#top') || href === '' ? 'body' : href;
scrollStopTarget = document.querySelector(selector);
}
return scrollStopTarget;
}
//アニメーション実行中class削除
const resetBodyBusyClass = () => {
bodyClassList.remove(SCROLL_BUSY_CLASS);
}
//スクロール停止位置取得
const getScrollStopPosition = (scrollStopTarget) => {
//スクロール位置の調整
//scroll-margin-top、scroll-padding-topを取得。相対値による小数点は四捨五入
const scrollStopTargetStyles = getComputedStyle(scrollStopTarget);
const scrollPaddingTop = scrollStopTargetStyles.scrollPaddingTop;
const shiftPosition = Math.round(parseFloat(scrollStopTargetStyles.scrollMarginTop)) + Math.round(parseFloat(scrollPaddingTop === 'auto' ? 0 : scrollPaddingTop));
//移動先取得
return Math.round(scrollStopTarget.getBoundingClientRect().top) - shiftPosition;
}
//フォーカス移動
const changeFocus = (scrollStopTarget) => {
scrollStopTarget.focus();
//ターゲット要素がフォーカス可能な要素以外の場合
if (document.activeElement !== scrollStopTarget) {
scrollStopTarget.setAttribute('tabindex', '-1');
scrollStopTarget.focus();
//フォーカスアウトラインを非表示
scrollStopTarget.style.outline = 'none';
}
}
//スクロール終了処理
const endFunc = (scrollStopTarget) => {
//アニメーション実行中class削除
resetBodyBusyClass();
//フォーカス移動
changeFocus(scrollStopTarget);
}
//スクロールアニメーション
const raf = requestAnimationFrame;
const scrollAnime = (scrollStopTarget,scrollStopPosition) => {
//現在のスクロール量
const scrollTop = window.scrollY;
//アニメーション開始時間
let startTime;
//アニメーション関数
const startAnime = (timestamp) => {
//イベント発生後の経過時間
// startTime未定義時のみtimestampを代入することで一度だけstartTimeに数値を格納する
if (startTime === undefined) {
startTime = timestamp;
}
// 経過時間を監視
const elapsedTime = timestamp - startTime;
//アニメーション終了処理
if (elapsedTime > ANIME_SPEED) {
endFunc(scrollStopTarget);
//処理を終了
return;
}
//進捗度を算出してイージングを適用
//elapsedTimeを0からANIME_SPEEDの値で正規化(0〜1の値に変換)
const elapsedTimeNorm = elapsedTime / ANIME_SPEED;
//位置設定
const scrollY = scrollTop + scrollStopPosition * easing(elapsedTimeNorm);
//スクロール処理
window.scrollTo(
0,
scrollY
);
//アニメーション関数再実行
raf(startAnime);
}
//初回アニメーション呼び出し
raf(startAnime);
}
//クリックイベント設定
targets.forEach((target) => {
target.addEventListener('click', event => {
event.preventDefault();
//スクロール停止要素取得
const scrollStopTarget = getScrollStopTarget(target);
//遷移先が存在しない場合は処理しない
if (!scrollStopTarget) {
resetBodyBusyClass();
return;
}
//スクロールイベント重複防止
if (bodyClassList.contains(SCROLL_BUSY_CLASS)) {
return;
}
bodyClassList.add(SCROLL_BUSY_CLASS);
//スクロール停止位置取得
const scrollStopPosition = getScrollStopPosition(scrollStopTarget);
//視差効果を減らす設定がある場合はアニメーションしない
if(matchMedia('(prefers-reduced-motion: reduce)').matches) {
window.scrollTo(
0,
scrollStopPosition + window.scrollY
);
endFunc(scrollStopTarget);
} else {
scrollAnime(scrollStopTarget,scrollStopPosition);
}
});
});
}
//スムーススクロール関数呼び出し
smoothScroll();
関数の呼び出し
任意の場所で以下を実行し関数を呼び出してください。
//スムーススクロール関数呼び出し
smoothScroll();
小数点まで正確に位置を割り出したい場合
小数点以下はMath.round()
で四捨五入して整数値に丸めています。
何らかの理由で正確に算出したい場合は、移動先の位置を取得する関数getScrollStopPosition()
内のMath.round()
を削除してください。
before
//スクロール停止位置取得
const getScrollStopPosition = (scrollStopTarget) => {
//scroll-margin-top、scroll-padding-topを取得。相対値による小数点は四捨五入
const scrollStopTargetStyles = getComputedStyle(scrollStopTarget);
const scrollPaddingTop = scrollStopTargetStyles.scrollPaddingTop;
/*
それぞれの値に Math.round() を設定して
小数点以下を四捨五入している
*/
const shiftPosition = Math.round(parseFloat(scrollStopTargetStyles.scrollMarginTop)) + Math.round(parseFloat(scrollPaddingTop === 'auto' ? 0 : scrollPaddingTop));
return Math.round(scrollStopTarget.getBoundingClientRect().top) - shiftPosition;
}
after
//スクロール停止位置取得
const getScrollStopPosition = (scrollStopTarget) => {
const scrollStopTargetStyles = getComputedStyle(scrollStopTarget);
const scrollPaddingTop = scrollStopTargetStyles.scrollPaddingTop;
/*
小数点まで正確に位置を割り出したい場合は、
3箇所設定している Math.round() を削除
*/
const shiftPosition = parseFloat(scrollStopTargetStyles.scrollMarginTop) + parseFloat(scrollPaddingTop === 'auto' ? 0 : scrollPaddingTop);
return scrollStopTarget.getBoundingClientRect().top - shiftPosition;
}