こんにちは、かつコーチです。
前回はフォームのバリデーションをjQueryで実装しました。
実践UI実装シリーズの最後は、ページ内リンクをなめらかに移動させるスムーススクロールです。
素のJavaScript版では scrollIntoView() や window.scrollTo() を使いましたが、jQueryでは animate() と scrollTop を組み合わせるのが定番の実装方法です。
古い案件のコードでよく見かける書き方でもあるので、保守案件を担当する人はぜひ押さえておいてください。
jQueryでのスムーススクロールの考え方
scrollTopプロパティとは
scrollTopとは、要素が縦方向にどれだけスクロールされているかを表す数値のことです。
$(window).scrollTop() のように引数なしで呼ぶと現在のスクロール位置を取得でき、$('html, body').scrollTop(100) のように数値を渡すと、その位置まで一瞬でジャンプさせられます。
素のJavaScript版の window.scrollTo() に近い役割ですが、jQueryでは animate() と組み合わせることで、この「一瞬のジャンプ」を「なめらかな移動」に変えるのが基本方針です。
なぜhtmlとbodyの両方を指定するのか
$('html, body').animate({ scrollTop: 100 }, 400);
ブラウザによって、スクロール位置を管理している要素が <html>(document.documentElement)だったり <body> だったりと差があります。
そのため、$('html, body') のように両方をまとめて指定し、対応しているほうだけがアニメーションするようにしておくのが、ブラウザ差異を避けるための定番の書き方です。
基本の実装手順
手順1:HTMLとCSSを用意する
<nav>
<a href="#section1" class="nav-link">セクション1へ</a>
<a href="#section2" class="nav-link">セクション2へ</a>
</nav>
<section id="section1">セクション1</section>
<section id="section2">セクション2</section>
section {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: white;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
HTML・CSSの構造は素のJavaScript版と変わりません。
手順2:jQueryでクリック時の処理を書く
$(function () {
$('.nav-link').on('click', function (event) {
event.preventDefault(); // デフォルトの一瞬ジャンプを止める
const targetId = $(this).attr('href'); // 例: "#section1"
const $target = $(targetId);
$('html, body').animate(
{ scrollTop: $target.offset().top },
600 // 600ミリ秒かけてスクロール
);
});
});
$target.offset().top は、「ページ全体で見たときの、その要素の上端の位置(縦方向の絶対座標)」を取得するメソッドです。
素のJS版で getBoundingClientRect().top + window.pageYOffset と計算していた処理が、offset().top の1行に置き換わっています。
手順3:固定ヘッダーの高さ分ずらす
固定ヘッダーがある場合、素のJS版と同じくヘッダーの高さを差し引く必要があります。
$(function () {
const headerHeight = $('nav').outerHeight();
$('.nav-link').on('click', function (event) {
event.preventDefault();
const targetId = $(this).attr('href');
const $target = $(targetId);
$('html, body').animate(
{ scrollTop: $target.offset().top - headerHeight },
600
);
});
});
outerHeight() は、要素の高さに padding や border を含めたサイズを取得できるメソッドです。
素のJS版の offsetHeight に近い値が取れますが、outerHeight(true) のように引数を true にすると margin まで含めた高さが取れる、という違いも覚えておくと便利です。
よくあるつまずきポイント・エラー対処
Before/After:animateが2回同時に動いて位置がズレる
私が実際にjQueryでスムーススクロールを実装したとき、$('html, body') の両方が同時にアニメーションしてしまい、想定よりも速く、かつガクついた動きになってしまったことがありました。
❌ Before:html/bodyの両方が常に反応する環境を想定していない
$('html, body').animate({ scrollTop: targetTop }, 600);
この書き方自体は間違いではないのですが、一部のブラウザ・環境では html と body の両方がスクロール位置を保持しており、アニメーションが二重に実行されて、想定より速く移動してしまうことがあります。
私はこの現象に気づかず「なぜかスクロール速度の指定(600ミリ秒)が効いていない」と勘違いし、原因調査に時間をかけてしまいました。
✅ After:stop(true)を挟んで多重実行を防ぐ
$('.nav-link').on('click', function (event) {
event.preventDefault();
const targetId = $(this).attr('href');
const $target = $(targetId);
$('html, body')
.stop(true) // 実行中のアニメーションとキューを止めてから
.animate({ scrollTop: $target.offset().top - headerHeight }, 600);
});
スライダーの実装のときと同様、animate() を使う場面では stop() によるキュー・多重実行対策をセットで考える習慣をつけておくと安心です。
Before/After:href属性がURLの場合にエラーになる
もう1つ、ナビゲーションのリンクにページ内リンク以外が混ざっているとエラーになる、というつまずきもよくあります。
❌ Before:href属性をそのままセレクタに渡している
$('.nav-link').on('click', function (event) {
event.preventDefault();
const $target = $($(this).attr('href')); // hrefが"https://example.com"だとエラーになる
$('html, body').animate({ scrollTop: $target.offset().top }, 600);
});
.nav-link クラスの中に <a href="https://example.com"> のような外部リンクが混ざっていると、$('https://example.com') という不正なセレクタが $() に渡されてしまい、コンソールに Syntax error, unrecognized expression というエラーが表示されます。
✅ After:ページ内リンク(#で始まる)かどうかを判定する
$('.nav-link').on('click', function (event) {
const href = $(this).attr('href');
if (!href.startsWith('#')) return; // ページ内リンク以外は通常通りの遷移に任せる
event.preventDefault();
const $target = $(href);
$('html, body').stop(true).animate({ scrollTop: $target.offset().top }, 600);
});
href.startsWith('#') で「#から始まるページ内リンクかどうか」を先に判定し、それ以外の場合は早期returnで処理を抜ける、というガード節を入れておくと安全です。
応用:easingで動きに緩急を付ける
swingとlinearを使い分ける
animate() のオプションで easing を指定すると、動きの印象を変えられます。
$('html, body').stop(true).animate(
{ scrollTop: $target.offset().top - headerHeight },
{
duration: 600,
easing: 'swing', // 始めと終わりが緩やかになる(jQuery標準の既定値)
}
);
linear は最初から最後まで一定速度、swing は始まりと終わりが緩やかになる動きです。
jQuery標準ではこの2種類のみですが、スムーススクロールのような「移動距離が短いUI」であれば、標準の swing で十分自然な動きになります。
まとめ
この記事のポイント
- jQueryのスムーススクロールは
scrollTopをanimate()でアニメーションさせる方式が定番 - ブラウザ差異を避けるため
$('html, body')と両方を指定するのが基本形 offset().topで要素の絶対座標を取得でき、固定ヘッダー分はouterHeight()を引いて調整するanimate()の多重実行対策としてstop(true)を挟む習慣をつけるhrefが#から始まるページ内リンクかどうかを判定するガード節を入れないと、外部リンクでセレクタエラーになる
次に読むべき記事
これで実践UI実装シリーズ(jQuery編)はひと通り実装できました。
次回は、$.ajax()を使ったAPI通信の基本を解説します。
→ 次の記事:$.ajax()でAPI通信する基本
タグ:jQuery, 初心者向け, UI実装