固定ヘッダーに被らない!GSAP(ScrollToPlugin)で作るスムーススクロール

Webサイト制作で「ページ内リンク(アンカーリンク)」を実装したとき、移動先の見出しが上部固定(追従)ヘッダーの裏側にすっぽり隠れてしまって困った経験はありませんか?

CSSの scroll-padding-top を使ってサクッと解決することもできますが、要件によっては「もう少し細かくスクロール位置を調整したい」「移動中のアニメーションをもっと滑らかにしたい」という場面も出てきますよね。

そこで今回は、JavaScriptの強力なアニメーションライブラリである「GSAP(ScrollToPlugin)」を活用して、ヘッダーの高さに依存しない、より細やかで完璧なスムーススクロールを実装する方法をご紹介します!

実際の動作デモ

まずはGSAPを使った実際のスクロールの動きを、以下のデモページで確認してみてください。

※リンクをクリックした際、要素がヘッダーの裏側に隠れず、上に綺麗な余白(24px)を保ったままピタッと止まる点に注目してください!

実装コードの紹介

今回のスムーススクロールを実装するための、基本的なHTML・CSS・JavaScriptのコードです。ご自身の環境に合わせてクラス名などを調整してご活用ください。

HTMLの構造

ページ上部に固定するヘッダー(ナビゲーション)と、スクロール先の各セクションを用意します。リンクは標準的なアンカーリンク(href="#ID名")の形式で記述します。

<header class="l-header">
  <nav class="l-nav">
    <a href="#section-1">セクション1へ</a>
    <a href="#section-2">セクション2へ</a>
    <a href="#section-3">セクション3へ</a>
  </nav>
</header>

<main class="l-main">
  <section id="section-1">
    <h2>セクション1</h2>
  </section>
  <section id="section-2">
    <h2>セクション2</h2>
  </section>
  <section id="section-3">
    <h2>セクション3</h2>
  </section>
</main>

CSSの設定

ヘッダーを position: fixed で上部に固定します。そのままではメインコンテンツの上部がヘッダーの裏に隠れてしまうため、ヘッダーの高さ分だけ padding-top を設けておくのがポイントです。

/* ヘッダーを上部に固定 */
.l-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 60px; /* JSで高さを取得するので、どんな値でも可 */
  z-index: 100;
}

/* ヘッダーの裏にコンテンツが隠れないように余白を設ける */
.l-main {
  padding-top: 60px; /* ヘッダーの高さ分下げる */
}

/* 各セクション(テスト用に高さを出しています) */
section {
  min-height: 100vh;
  padding: 40px 20px;
}

JavaScriptの実装

GSAP本体と ScrollToPlugin を読み込んだ上で、以下のJavaScriptを記述します。
リンクをクリックした際、デフォルトの瞬間移動をキャンセルし、GSAPのアニメーション処理へと繋ぎます。

document.addEventListener("DOMContentLoaded", () => {
  // GSAP ScrollToPluginの登録
  gsap.registerPlugin(ScrollToPlugin);

  // ページ内リンク(#から始まるhref)をすべて取得
  const anchorLinks = document.querySelectorAll('a[href^="#"]:not([href="#"])');

  anchorLinks.forEach(link => {
    link.addEventListener('click', (e) => {
      e.preventDefault(); // デフォルトの瞬間移動をキャンセル

      const targetId = link.getAttribute('href');

      if (targetId && targetId !== '#') {
        const targetElement = document.querySelector(targetId);

        if (targetElement) {
          // 1. ヘッダーの高さを動的に取得
          const header = document.querySelector('.l-header');
          const headerHeight = header ? header.offsetHeight : 0;

          // 2. ヘッダーの高さ + 少しの余白(24px)を計算
          const offset = headerHeight + 24;

          // 3. GSAPで滑らかにスクロール
          gsap.to(window, {
            duration: 0.6,
            ease: "power2.out",
            scrollTo: {
              y: targetElement,
              offsetY: offset,
              autoKill: true, // アニメーション中にユーザーが操作したら止める
            }
          });
        }
      }
    });
  });
});

コードの解説・こだわったポイント

ここからは、今回のJavaScript実装において特にこだわった「3つのポイント」を解説します。ただ動くだけではなく、保守性やUX(ユーザー体験)を考慮した設計にしています。

ヘッダーの高さを「動的」に取得する

固定ヘッダーの高さは、スマートフォン表示(SP)とパソコン表示(PC)で異なるケースがほとんどです。
もしJS側に固定の数値(例:60pxなど)をハードコーディングしてしまうと、後からデザイン変更でヘッダーの高さが変わった際に、スクロール位置がズレるバグの原因になってしまいます。

// 1. ヘッダーの高さを動的に取得
const header = document.querySelector('.l-header');
const headerHeight = header ? header.offsetHeight : 0;

そのため今回は offsetHeight を用いて、リンクがクリックされた瞬間の「実際のヘッダーの高さ」をJavaScriptで毎回計算して取得する設計にしています。

余白(オフセット)の追加で息苦しさをなくす

GSAPの offsetY に「ヘッダーの高さ(headerHeight)」だけを指定してスクロールを止めると、移動先の見出しテキストがヘッダーの下端にギリギリくっついてしまい、視覚的に窮屈な印象を与えてしまいます。

// 2. ヘッダーの高さ + 少しの余白(24px)を計算
const offset = headerHeight + 24;

今回は const offset = headerHeight + 24; とすることで、上に24pxの余白(デザイン上のマージン)を意図的に空けてピタッと止まるように調整し、ユーザーにとって読みやすいレイアウトを保っています。

UXを高めるautoKill: trueの設定

GSAPの ScrollToPlugin を使う最大のメリットとも言えるのが、autoKill: true というオプション設定です。

scrollTo: {
    y: targetElement,
    offsetY: offset,
    autoKill: true, // アニメーション中にユーザーが操作したら止める
}

これは、「スムーススクロールで画面が自動移動している最中に、ユーザーが自分でマウスホイールを回したりスワイプしたり(手動スクロール)した場合、GSAPのアニメーションを強制終了してユーザーの操作を優先する」という機能です。
これがないと「画面が勝手に動いて自分の操作が効かない!」というユーザーストレス(スクロールジャック)に繋がるため、UXを向上させるために非常に重要な設定です。

まとめ

今回はGSAPの ScrollToPlugin を使って、固定ヘッダーに被らないスムーススクロールを実装する方法をご紹介しました!

CSSの scroll-padding-top だけでシンプルに完結させることもできますが、JavaScript(GSAP)を活用することで、高さを動的に計算してレイアウト崩れを防いだり、ユーザーのスクロール操作を優先(autoKill: true)できたりと、より細やかでユーザーファーストなUXを実現できます。

当サイト「Mikanbako Docs」でも実際にこの仕組みを取り入れているので、ぜひページ内のアンカーリンクでその滑らかさを体感してみてくださいね!
今後もフロントエンドのちょっとした工夫や、こだわりの実装方法を発信していくのでよろしくお願いします!

Next