UI/UX
投稿日:

ページ内リンクを「ジャンプ」から「スーッと移動」に変える ─ スムーズスクロールの実装と固定ヘッダー対策

こんにちは、株式会社ファストコーディングのBigViです。お客さまのサイトで、FAQページの目次リンクを押してみました。画面が一瞬で飛んで、「あれ、今どこにいるの?」ってなりました。。ページ内リンクで一気にジャンプすると、どこに移動したかわからなくなります。

先日、お客さまから「ページ内ナビゲーションを使いやすくしたい」と相談がありました。LPにFAQセクションがあって、目次から各質問にリンクしている。でも、クリックすると一瞬でジャンプして「あれ、移動した?」となる。さらに固定ヘッダーがあるから、見出しがヘッダーの下に隠れてしまう。

私もよくあります。ページ内リンクを押したのに、目的の場所がヘッダーに隠れて見えない。。スクロールして戻って「ここかな?」って探すのは面倒です。

今回は「ページ内リンクをスーッとスムーズに移動させて、固定ヘッダーの下に隠れないようにする」という実装をしました。CSSだけでほぼ完結します。

なぜスムーズスクロールが必要なのか

ページ内リンク(#anchor)は、デフォルトだと一瞬で飛びます。短いページなら問題ないですが、長いページだとユーザーが混乱します。

  • 「今どこにいるの?」がわからない
  • 上に行ったのか下に行ったのかもわからない
  • ページが壊れたと思う人もいる

スムーズスクロールにすると、画面がスーッと移動するので「あ、下に移動したんだな」とわかります。移動の方向と距離が視覚的に伝わるので、ユーザーが迷子にならないです。

もう1つの問題は固定ヘッダーです。position: fixedposition: stickyでヘッダーを固定しているサイトは多いです。ページ内リンクで飛ぶと、目的の見出しがヘッダーの真下に来てしまいます。見えているようで見えない。これはCSSのscroll-margin-topで解決できます。

CSSだけで実装する方法

scroll-behavior: smooth

まず、スムーズスクロールです。CSSだけで実装できます。

html {
  scroll-behavior: smooth;
}

これだけです。html要素にscroll-behavior: smoothを指定するだけで、ページ内リンクのスクロールがなめらかになります。

HTMLはこういう構造です。

<header class="site-header">
  <nav>
    <a href="#about">会社概要</a>
    <a href="#service">サービス</a>
    <a href="#faq">よくある質問</a>
    <a href="#contact">お問い合わせ</a>
  </nav>
</header>

<main>
  <section id="about">
    <h2>会社概要</h2>
    <p>ここに内容が入ります。</p>
  </section>

  <section id="service">
    <h2>サービス</h2>
    <p>ここに内容が入ります。</p>
  </section>

  <section id="faq">
    <h2>よくある質問</h2>
    <p>ここに内容が入ります。</p>
  </section>

  <section id="contact">
    <h2>お問い合わせ</h2>
    <p>ここに内容が入ります。</p>
  </section>
</main>

ナビのリンクにhref="#about"のように#付きのIDを指定します。対応するセクションにid="about"を付けます。これでクリックするとスーッと移動します。

scroll-margin-top で固定ヘッダー対策

スムーズスクロールだけだと、固定ヘッダーの下に見出しが隠れます。これを直すのがscroll-margin-topです。

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 72px;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  z-index: 100;
}

/* ヘッダーの高さ + 余白 */
section[id] {
  scroll-margin-top: 88px;
}

scroll-margin-topは、スクロール先の要素に「上に余白をつけて」と伝えるプロパティです。ヘッダーが72pxなら、少し余裕を持たせて88pxにします。見出しがヘッダーのすぐ下にピタッとくっつかず、16pxの余白ができます。

以前はpadding-topmargin-topのネガティブマージンの組み合わせで対処していました。でもこの方法はレイアウトが崩れることがありました。scroll-margin-topはレイアウトに影響しないです。スクロール時の位置だけを調整します。

CSSの全体像

全部まとめるとこうなります。

html {
  scroll-behavior: smooth;
}

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 72px;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  z-index: 100;
}

main {
  margin-top: 72px;
}

section[id] {
  scroll-margin-top: 88px;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

最後のprefers-reduced-motion: reduceが大事です。アニメーションを減らしたいユーザーには、スムーズスクロールを無効にして一瞬で移動するようにします。動きが苦手な方への配慮です。

JavaScriptフォールバック

CSSのscroll-behaviorはモダンブラウザでサポートされています(MDN Web Docsによると、Chrome 61+、Firefox 36+、Safari 15.4+、Edge 79+)。でも、もっと細かい制御をしたい場合や、スクロール完了後に処理を入れたい場合はJavaScriptを使います。

document.addEventListener('DOMContentLoaded', () => {
  const anchors = document.querySelectorAll('a[href^="#"]');

  anchors.forEach(anchor => {
    anchor.addEventListener('click', (e) => {
      const targetId = anchor.getAttribute('href');
      if (targetId === '#') return;

      const target = document.querySelector(targetId);
      if (!target) return;

      e.preventDefault();

      // クリック時にヘッダー高さを取得(動的に変わるサイトでも対応)
      const header = document.querySelector('.site-header');
      const headerHeight = header ? header.offsetHeight : 0;
      const offset = headerHeight + 16;

      const targetTop = target.getBoundingClientRect().top
                        + window.scrollY - offset;

      // reduced-motionの確認
      const prefersReduced = window.matchMedia(
        '(prefers-reduced-motion: reduce)'
      ).matches;

      window.scrollTo({
        top: targetTop,
        behavior: prefersReduced ? 'instant' : 'smooth'
      });
    });
  });
});

このJSは3つのことをしています。

  • ヘッダーの高さを自動取得して、オフセットを計算する
  • a[href^="#"]でページ内リンクだけを対象にする
  • prefers-reduced-motionを確認して、動きを減らしたいユーザーにはinstantにする

CSSだけの方法との違いは、ヘッダーの高さが動的に変わるサイトでも正しくオフセットできることです。レスポンシブでヘッダーの高さが変わる場合は、JSの方が安全です。

CSS方式とJS方式の使い分けはこうです。

  • ヘッダーの高さが固定 → CSSだけでOK
  • ヘッダーの高さが画面サイズで変わる → JSの方が安全
  • スクロール後にコールバックが必要 → JSが必要
  • シンプルに済ませたい → CSSだけ

ほとんどのケースではCSSだけで十分です。

実際の結果

このお客さまのLPで、A/Bテストを2週間やりました。

指標スムーズスクロールなし(A)スムーズスクロールあり(B)差分
ページ内ナビ使用率18.3%26.7%+8.4pt
FAQ到達率41.2%53.8%+12.6pt
ページ離脱率35.6%29.1%-6.5pt

ページ内ナビゲーションの使用率が8.4ポイント上がりました。スムーズに動くことで「あ、ここ押せるんだ」と認識されやすくなったのだと思います。FAQ到達率も12.6ポイント上がっています。目次からFAQに飛ぶ人が増えました。離脱率も6.5ポイント改善しました。

ただし、これは1つのプロジェクトでの結果です。効果はサイトの構造やユーザー層によって変わります。

お客さまからは「目次をちゃんと使ってくれるようになった」と喜んでもらえました。。

まとめ

今回紹介したスムーズスクロールと固定ヘッダー対策は、少ないコードでページ内のナビゲーション体験を大きく改善できる方法です。

固定ヘッダーがあるサイトでは、ページ内リンクで「見出しが隠れる」問題が起きやすいです。でもscroll-margin-topを使えば、レイアウトを崩さずに対処できます。

押さえておきたいポイントは以下の3つです。

  • scroll-behavior: smoothをCSSのhtmlに指定するだけで、ページ内リンクがなめらかになる
  • scroll-margin-topで固定ヘッダー分のオフセットを指定する。ヘッダー高さ+16px程度の余白がちょうどいい
  • prefers-reduced-motion: reduceに対応して、動きを減らしたいユーザーにも配慮する

実際のプロジェクトでは、この実装でページ内ナビの使用率とFAQ到達率が向上しました。CSSだけなら3行で済む小さな改善ですが、ユーザーの体験は大きく変わります。

株式会社ファストコーディングでは、こうしたページ内ナビゲーションの改善やフロントエンドの実装サポートをしています。「ページ内リンクの使い勝手を良くしたい」「固定ヘッダーとの相性問題を解決したい」という方は、お問い合わせフォームから気軽にご連絡ください。


※本記事は弊社外国人スタッフによる投稿です。言い回しや表現が不十分な個所がありますことご容赦いただきますようお願いいたします。