【jQuery】スムーススクロールをjQueryで実装する

JavaScript

こんにちは、かつコーチです。

前回はフォームのバリデーションを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() は、要素の高さに paddingborder を含めたサイズを取得できるメソッドです。

素のJS版の offsetHeight に近い値が取れますが、outerHeight(true) のように引数を true にすると margin まで含めた高さが取れる、という違いも覚えておくと便利です。

よくあるつまずきポイント・エラー対処

Before/After:animateが2回同時に動いて位置がズレる

私が実際にjQueryでスムーススクロールを実装したとき、$('html, body') の両方が同時にアニメーションしてしまい、想定よりも速く、かつガクついた動きになってしまったことがありました。

❌ Before:html/bodyの両方が常に反応する環境を想定していない

$('html, body').animate({ scrollTop: targetTop }, 600);

この書き方自体は間違いではないのですが、一部のブラウザ・環境では htmlbody の両方がスクロール位置を保持しており、アニメーションが二重に実行されて、想定より速く移動してしまうことがあります。

私はこの現象に気づかず「なぜかスクロール速度の指定(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のスムーススクロールはscrollTopanimate()でアニメーションさせる方式が定番
  • ブラウザ差異を避けるため$('html, body')と両方を指定するのが基本形
  • offset().topで要素の絶対座標を取得でき、固定ヘッダー分はouterHeight()を引いて調整する
  • animate()の多重実行対策としてstop(true)を挟む習慣をつける
  • href#から始まるページ内リンクかどうかを判定するガード節を入れないと、外部リンクでセレクタエラーになる

次に読むべき記事

これで実践UI実装シリーズ(jQuery編)はひと通り実装できました。

次回は、$.ajax()を使ったAPI通信の基本を解説します。

→ 次の記事:$.ajax()でAPI通信する基本

タグ:jQuery, 初心者向け, UI実装

タイトルとURLをコピーしました