みなさん、スムーススクロール(慣性スクロール)してますか。
jQueryの世界ではいにしえより伝わる以下のコードがあり、 # で始まるIDへのアンカーリンクの「スクロールがヌルっと動くため」だけにコピペで動くコードとして重宝されてきました。
$(function(){
$('a[href^="#"]').click(function(){
var speed = 400; // ページ中のどこに行くにも4秒で着く
var href= $(this).attr("href");
var target = $(href == "#" || href == "" ? 'html' : href);
var position = target.offset().top;
$('body,html').animate({scrollTop:position}, speed, 'swing');
return false;
});
});
ただ近年、このようなスムーススクロールを実装したい「だけ」であれば、以下のCSS1行の記述だけで完結するようになりました。
html {
scroll-behavior: smooth; /* これだけ……だが…… */
}
イージング(アニメーションの緩急)やデュレーション(辿り着くまでの時間=スピード)は各種ブラウザの取り決めに従うため調整用のオプションはありませんが、先ほどのjQueryで行っていたスムーススクロール程度の動き・要望についてはこちらのCSSのみで事足りる便利な記述です。
日本ではモバイル天下一ブラウザであるSafariについても2022年から(遅くない……?)対応しているためほぼ問題なく活用でき、実際 scroll-behavior: smooth; について調べると、滑らかなスクロールはこれだけで解決! という記事ばかり引っかかるようになりました。めでたしめでたし
……はたして本当にそうでしょうか……?
全てのスクロールバグの生みの親
たとえばLPペライチ内でのアンカーリンクのためだけにスムーススクロールの処理を書くのは……といった程度の場合には先ほどのCSSによる置き換えが有効ではあります。
ただし、サイトの規模が大きくなるほど scroll-behavior: smooth; は実用に耐えない場面が多いです。
つまりWeb記事の通り「これだけで解決!」と単純に思って reset.css や style.css の頭なんかに書いてしまうと他プラグインとの兼ね合いが悪く、原因不明のスクロール関連バグにつまづくことが多くなり……結局便利なスムーススクロールを使えなくなってしまう場面が稀にあります。
悪さをしている事例
原因不明といいつつほとんどは先の scroll-behavior: smooth; の記述があるためなのですが、具体的には以下のような事例が挙げられます。
-
https://seeder.site/web/610.html
Swiper.jsでいつものように無限ループさせていたら、1周目を終わろうとしていたときに高速移動している。
-
https://note.com/chino8234/n/n538c02cf5dd0
「アンカーリンクでTOPページの特定のセクションに飛ばそうとしたのに、なぜか毎回“次のセクション”に遷移してしまう…」
-
https://x.com/tak_dcxi/status/1781708160484692054?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1781708160484692054%7Ctwgr%5E6865e621c16247d4df7f699d0f88d3aa328110a0%7Ctwcon%5Es1_c10&ref_url=https%3A%2F%2Fwww.tak-dcxi.com%2Farticle%2Fsmooth-scroll-implementation-example%2F (https://www.tak-dcxi.com/article/smooth-scroll-implementation-example/ )
ページ遷移後にスクロールで待たせる挙動はCSSのスムーススクロールでも同じ。JSならこの挙動を無効にできるけどCSSではできない。
-
https://x.com/tami_design/status/1815737711099248742?s=20
GSAPのScrollTriggerとscroll-behavior: smooth;は同時に使用しない
横スクロールを実装したのですがリロードや画面リサイズすると表示が崩れたりして安定しなかった…※こちら、GSAPのアニメーションがやけに重いと思ったら原因これだったので実体験です。
主にスライダー(カルーセル)やスクロールアニメーション系のプラグイン、ページ遷移時のアニメーションによくない影響を及ぼしていることがわかります。近頃はこういったプラグインもアニメーションの軽量化のためCSS animationを活用していることが多いのですが、そちらとCSS側の「全てをスムーススクロール」させる命令が干渉・衝突しているためかと思われます。
しかも scroll-behavior: smooth; は html に当てないとその効力を発揮しないため、必ずグローバル(ページ内すべて)のスクロールに対して反映されることになり……、衝突は避けられません。これではいけませんね
こういった問題が発生した時の対策も必ず「scroll-behavior: smooth;を外してCSSでスムーススクロールしない」になってしまうため、実装をラクしたサボったツケを喰らうことになります。
そこでサイトの規模が大きくなりそうだけどCSSではなく、かつjQueryにも依存しないJavaScriptでスムーススクロールするための代替コードを考えてみました。
scroll-behavior: smooth; の代替メソッド
具体的に辿り着きたい対象=ターゲットがはっきりしている場合、JSでは scrollIntoView() というメソッドを活用することができます。こちらに behavior: "smooth" という引数を与えると、CSSで行っていたスムーススクロールと同じ動きを行うことができます。
element.scrollIntoView({
behavior: "smooth", /* CSSで指定したものと同じ振る舞い */
block: 'start' /* 上揃えの位置にスクロールする */
});
ここでいう element (または window )がスムーススクロールで辿り着きたい対象となります。
また、この behavior: "smooth" は直接スクロール位置の座標を指定する scrollTo() メソッドでも利用することができます。
window.scrollTo({
behavior: "smooth", /* CSSで指定したものと同じ振る舞い */
top: 0 /* topからの位置が0 → ページトップへのスクロール */
});
/* 余談:scrollIntoView()で書きたい場合はelementを「document.body」とするとよいらしい
document.body.scrollIntoView({
behavior: "smooth",
block: 'start'
});
*/
どちらのメソッドであってもCSSで scroll-behavior: smooth; とした際と全く同じブラウザ側で定められた動きのスムーススクロールが行われます。
実用例
例えば冒頭のjQueryによる記載は以下のように置換できます。
const anchors = document.querySelectorAll('a[href^="#"]'); // 「#」で始まるアンカーリンクすべてを取得
anchors.forEach(anchor => {
// アンカーリンクそれぞれにクリック時のイベントを設定
anchor.addEventListener('click', (e) => {
e.preventDefault(); // 本来のアンカーリンク挙動によるスクロールをキャンセル
const href = anchor.getAttribute('href');
if ( href === '#' || href == "" ) {
// hrefが「#」のみまたは空=ページトップに戻る場合
window.scrollTo({
behavior: "smooth", // スムーススクロール
top: 0 // ページトップ
});
} else {
// hrefが「#◯◯◯」の場合
const target = document.querySelector(href);
if (target) {
// 「#◯◯◯」のID=アンカーリンク先が見つかった場合、その位置にスクロール
target.scrollIntoView({
behavior: 'smooth', // スムーススクロール
block: 'start' // 上端合わせ
});
}
}
});
});
また繰り返すようですが behavior: "smooth" によるスムーススクロールはCSSと同じくブラウザ側でスクロール挙動を行うため、 scroll-margin-top のようなCSSによるスクロール位置の調整も問題なく適用され(てしまい)ます。
シンプルな固定ヘッダー分だけずらすようであれば以下のようなクラスを用意しておけばわざわざJSでヘッダーの高さを取得してスクロール位置を計算する……などの処理は必要ありません。
(固定ヘッダーの高さが幅によって可変する場合は、きちんとJSで計算する必要がありますが……)
.anchor-target {
scroll-margin-top: 100px; /* ヘッダーの高さ分スクロール位置をずらす */
}
※上記CSSの適用には :target 疑似要素を使う方法もありますが、今回は e.preventDefault(); によって本来のアンカーリンク挙動をキャンセルしているため、スクロールの際 # で始まるハッシュ(URLフラグメント)が付かない= :target 要素が機能しません。
そのため仮に .anchor-target というクラスをアンカーリンクさせたいID位置に付ける想定をしています。
(他にルート相対 /#◯◯◯ で始まる場合などの考慮も場合によっては必要ですが)あくまでページ内アンカーリンクのスムーススクロール化であれば上記のJSで事足りるかと思います。
便利なプロパティほど思わぬところや実装が進んだところでのつまづきに繋がるので、スムーススクロール「だけ」の機能であっても正しく・意図した挙動になるよう心がけて実装しましょう。