scroll-margin-topを使った固定ヘッダーでのアンカーリンク対応
開発環境
- 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-top と padding-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 で触る機会があれば、ぜひ一度試してみてください。
参照
- scroll-margin-top - MDN https://developer.mozilla.org/ja/docs/Web/CSS/scroll-margin-top
- Scroll Margin - Tailwind CSS https://tailwindcss.com/docs/scroll-margin