logo

scroll-margin-topを使った固定ヘッダーでのアンカーリンク対応

2026-06-07
6 days ago

開発環境

  • React 19
  • Tailwind CSS 4

前提

固定ヘッダー(fixed / sticky)のあるサイトで、ページ内アンカーリンクを実装したことがある人向けの内容です。

固定ヘッダーがあるサイトで、#section のようなアンカーリンクを踏むと、こんな経験はないでしょうか。

ジャンプ先の見出しがヘッダーの裏に隠れて見えない。

これはアンカー先の要素がビューポートの最上部にスクロールするため、その上に被さっている固定ヘッダーにコンテンツが隠れてしまうのが原因です。

見た目が崩れるだけでなく、キーボード操作やスクリーンリーダーでフォーカス移動したときにも「今どこを見ているのか」が分からなくなり、アクセシビリティ上の課題にもなります。

たとえば、こんなよくある構成を想像してください。

const Page = () => (
  <>
    <header className="fixed top-0 h-20 w-full">ヘッダー</header>
    <main>
      <section id="about">About</section>
    </main>
  </>
);

// <a href="#about"> を踏むと、About が h-20 のヘッダーの裏に隠れる

本題

よくある対処法:負の margin と padding

⚠️ 問題:昔からよく使われるのが、アンカー先の要素に負の margin-toppadding-top を組み合わせる方法です。Tailwind だとこうなります。

<section id="about" className="-mt-20 pt-20">
  About
</section>

ヘッダーの高さ分だけ要素を上にずらし、その分 padding で押し戻すことで、見かけ上の位置を保ちつつスクロール位置だけをずらすという発想です。

ただ、この方法にはいくつか欠点があります。

  • padding が増えるので、要素のクリック範囲やレイアウトに意図しない影響が出る
  • 要素そのものを触れない場合、空の <span> を埋め込むなどの回避策が必要になる
  • 見出しごとに mt-20 pt-20 を当てて回るので、保守がだんだん辛くなる

レイアウトのためではなく「スクロール位置の調整」のためにレイアウト用のプロパティを流用しているのが、そもそもの歪みだと感じます。

改善:scroll-mt を使う

🛠 改善:そこで使えるのが Tailwind の scroll-mt-*scroll-margin-top)です。

このユーティリティは「スクロールしてこの要素を表示するとき、上側にどれだけ余白を取るか」だけを指定します。通常のレイアウトには一切影響しません。

<section id="about" className="scroll-mt-20">
  About
</section>

scroll-mt-20 はヘッダーの高さ(h-20 = 5rem)に揃えています。これだけで About がヘッダーに隠れなくなります。

負の margin と違い、スクロール時にしか作用しないため、見た目のレイアウトは何も崩れません。

スクロールコンテナ側にまとめて指定する

セクションごとに scroll-mt-20 を書くのが面倒なら、スクロールする側(基本は html)に scroll-pt-*scroll-padding-top)を一括で当てる手もあります。

// Tailwind v4 では @layer base などで html に当てるか、
// ルート要素にクラスを付ける
<html className="scroll-pt-20">

👉 ポイント scroll-mt-* は「飛び先の要素」に余白を持たせる、scroll-pt-* は「スクロールする側(html)」に余白を持たせる、という違いです。サイト全体で一律にオフセットしたいなら scroll-pt-* を一度だけ当てるのがシンプルです。

ヘッダー高さを1箇所で管理する

ヘッダーの高さが画面幅で変わる場合や、値を一元管理したい場合は、CSS変数を Tailwind v4 のテーマに登録しておくと扱いやすくなります。

/* globals.css */
@theme {
  --spacing-header: 5rem;
}
// ヘッダーとオフセットで同じ値を参照できる
<header className="fixed top-0 h-(--spacing-header) w-full">ヘッダー</header>
<html className="scroll-pt-(--spacing-header)">

ヘッダーの高さとスクロールオフセットが同じ変数を見るので、値を変えたいときの修正漏れを防げます。

レスポンシブで高さを変えたい場合は md:scroll-pt-20 のようにブレークポイント付きで上書きすればOKです。

すぐ試せるサンプル

挙動を体感したい場合は、この1ファイルをそのまま貼れば動きます。scroll-mt-20 を消すと見出しがヘッダーに隠れる、付けると隠れない、という差を確認できます。

export default function AnchorDemo() {
  return (
    <>
      <header className="fixed top-0 z-10 flex h-20 w-full items-center gap-4 bg-gray-800 px-4 text-white">
        <a href="#about" className="underline">
          About
        </a>
        <a href="#contact" className="underline">
          Contact
        </a>
      </header>

      <main className="pt-20">
        {/* scroll-mt-20 を外すと、見出しがヘッダーの裏に隠れる */}
        <section id="about" className="flex h-screen scroll-mt-20 items-start bg-blue-50">
          <h2 className="text-2xl font-bold">About</h2>
        </section>
        <section id="contact" className="flex h-screen scroll-mt-20 items-start bg-green-50">
          <h2 className="text-2xl font-bold">Contact</h2>
        </section>
      </main>
    </>
  );
}

各セクションを h-screen にしてあるので、リンクを踏むとちゃんとスクロールが発生します。scroll-mt-20 の有無を切り替えて、見出しの止まる位置が変わるのを見比べてみてください。

さいごに

アンカーリンクがヘッダーに隠れる問題は、地味ですがユーザー体験にもアクセシビリティにも効いてくる部分です。

負の margin を使った対処は動くには動きますが、レイアウト用のプロパティを目的外に流用している分、副作用や保守の手間がついて回ります。

Tailwind の scroll-mt-* / scroll-pt-* は「スクロール位置の調整」という意図がそのままクラス名に出ているので、コードを読んだときに何をしているかが一目で分かります。

固定ヘッダーのサイトを React + Tailwind で触る機会があれば、ぜひ一度試してみてください。

参照