ページ内リンク(#id へのジャンプ)を押すと、目的の見出しへスクロールしてくれます。ところが画面上部に固定ヘッダーがあると、ジャンプ先の見出しがヘッダーの裏に隠れてしまい、少しだけ位置がズレて見えることがあります。この記事では、その位置ズレを CSS だけで直す scroll-margin-top と scroll-padding-top の使い方を、実際にスクロールして試せるデモを交えて解説します。似ているようで役割が違う「scroll-margin」と「scroll-padding」の違い、各方向のプロパティ、scroll-snap との関係まで順番に見ていきます。
目次
アンカーリンクで見出しが固定ヘッダーに隠れる理由
<a href="#section2"> のようなアンカーリンクを押すと、ブラウザは id="section2" を持つ要素の上端が、スクロール領域のちょうど一番上に来るようにスクロールします。このとき、ブラウザは「固定ヘッダーがそこに重なっている」ことを知りません。position: fixed や position: sticky のヘッダーは、あくまで他の要素の上に重ねて表示されているだけで、ジャンプ先の計算には関わらないためです。
その結果、目的の見出しは画面の一番上まで来るものの、その位置にはヘッダーが重なっているため、見出しがヘッダーの裏に潜り込んで見えなくなります。かつては透明な余白要素をアンカー用に挟んだり、JavaScript でスクロール位置を補正したりして対応していましたが、今は CSS の scroll-margin-top ひとつで解決できます。
scroll-margin-top でジャンプ先に余白を作る
scroll-margin-top は、その要素へスクロールして止まるときに、上側にどれだけ余白を空けて止めるかを指定するプロパティです。ジャンプ先の要素(見出しなど)に、固定ヘッダーの高さと同じか少し大きい値を指定します。ヘッダーの高さが 60px なら、次のように書きます。
/* ジャンプ先になる見出しに余白を持たせる */
h2[id],
h3[id] {
/* 固定ヘッダー(60px)ぶん上に余白を空けて止める */
scroll-margin-top: 60px;
}
大切なのは、この余白がスクロールして止まる位置の計算にだけ効くという点です。通常の margin-top のように見出しの周りに実際の余白ができるわけではなく、レイアウトの見た目は一切変わりません。あくまで「ジャンプで止まる位置を、指定したぶんだけ下にずらす」ための専用プロパティだと考えると分かりやすいです。値には px のほか、ヘッダーの高さに合わせやすい rem や、ビューポート基準の vh なども使えます。
スクロールして効果を確かめる
下のデモは、上部に固定ヘッダー(メニュー)があり、その中でスクロールできる小さなページです。メニューのリンクを押すと、対応する見出しへジャンプします。各セクションに scroll-margin-top を指定しているので、見出しがヘッダーの下にきちんと余白を空けて止まります。CSS タブの scroll-margin-top: 56px; の値を 0 に変えてから同じリンクを押すと、見出しがヘッダーの裏に隠れてしまう「ズレた状態」を再現できます。
<div class="page">
<header class="page__header">
<nav class="page__nav">
<a href="#sec1">説明</a>
<a href="#sec2">料金</a>
<a href="#sec3">よくある質問</a>
</nav>
</header>
<main class="page__body">
<section id="sec1" class="section">
<h2>説明</h2>
<p>上のメニューをクリックすると、その見出しへジャンプします。見出しには scroll-margin-top を付けているので、固定ヘッダーに隠れません。</p>
</section>
<section id="sec2" class="section">
<h2>料金</h2>
<p>ジャンプ先の見出しが、ちょうどヘッダーの下に余白を空けて止まります。scroll-margin-top を消すと、ヘッダーの裏に隠れてしまいます。</p>
</section>
<section id="sec3" class="section">
<h2>よくある質問</h2>
<p>この余白はスクロール位置の計算にだけ効くので、見た目のレイアウトはそのままです。</p>
</section>
</main>
</div>
.page {
height: 260px; /* デモ用にスクロールできる高さ */
overflow-y: auto; /* この中でスクロールさせる */
position: relative;
scroll-behavior: smooth; /* ジャンプを滑らかに */
font-family: sans-serif;
border: 1px solid #ddd;
border-radius: 8px;
}
.page__header {
position: sticky; /* 上に貼り付く固定ヘッダー */
top: 0;
height: 48px;
background: #1f2937;
z-index: 1;
}
.page__nav {
display: flex;
gap: 16px;
height: 100%;
align-items: center;
padding: 0 16px;
}
.page__nav a {
color: #fff;
text-decoration: none;
font-size: 14px;
}
.section {
padding: 16px;
border-bottom: 1px solid #eee;
/* ↓ ここがポイント。ヘッダーの高さ(48px)ぶん余白を空けて止める */
scroll-margin-top: 56px;
}
.section h2 {
margin: 0 0 8px;
font-size: 18px;
color: #2563eb;
}
.section p {
margin: 0;
line-height: 1.8;
color: #333;
}
/* 最後のセクションを高くして、上の見出しまでスクロールできるようにする */
#sec3 {
min-height: 200px;
}
scroll-margin と scroll-padding の違い
同じ位置ズレを直すプロパティに scroll-padding-top もあります。どちらもヘッダーぶんの余白を作る点は同じですが、余白を付ける相手が違います。scroll-margin はスクロールして止まる側の要素(見出しなどジャンプ先)に付ける余白です。一方 scroll-padding はスクロールするコンテナ側(overflow を持つ親要素や、ページ全体なら html)に付ける、内側の余白です。
| プロパティ | 付ける相手 | 意味 |
|---|---|---|
scroll-margin | スクロール先の要素(見出しなど) | その要素を止めるときに空ける外側の余白 |
scroll-padding | スクロールするコンテナ(html など) | スクロール領域の内側に確保する余白 |
固定ヘッダーの位置ズレ対策としては、どちらを使っても同じ見た目にできます。ただし考え方が異なります。scroll-padding-top はコンテナ側に一度だけ指定すれば、その中のすべてのジャンプ先に一律で余白が効くため、ページ全体で固定ヘッダーを避けたいときに向いています。多くの場合はこちらがシンプルです。次のように、スクロールするコンテナ(ページ全体なら html 要素)に指定します。
/* ページ全体のスクロールに対して、上に60pxの余白を確保する */
html {
scroll-padding-top: 60px;
}
これだけで、ページ内のどのアンカーへジャンプしても、上に 60px の余白が空いた位置で止まるようになります。ジャンプ先ごとに個別のスタイルを書く必要がありません。逆に scroll-margin は、特定の要素だけ止まる位置を調整したいときに便利です。たとえば「この見出しだけもう少し下で止めたい」といった細かい制御は、その要素に scroll-margin-top を指定して行います。
上下左右それぞれの方向を指定する
どちらのプロパティにも、margin や padding と同じように方向ごとの個別プロパティと一括指定があります。固定ヘッダー対策では上方向の -top だけをよく使いますが、横スクロールのカルーセルなどでは -left が活躍します。
| プロパティ | 効果 |
|---|---|
scroll-margin-top | スクロール先要素の上側の余白(固定ヘッダー対策の定番) |
scroll-margin-bottom | 下側の余白 |
scroll-margin-left / scroll-margin-right | 左・右の余白(横スクロール時に使う) |
scroll-margin | 四方向をまとめて指定する一括指定 |
scroll-padding-top | コンテナ上側の内余白(固定ヘッダー対策の定番) |
scroll-padding | コンテナの四方向をまとめて指定する一括指定 |
一括指定は margin と同じ書き方で、scroll-margin: 60px 0; のように「上下 / 左右」の順で複数の値を渡せます。scroll-padding も同様です。ふだんの固定ヘッダー対策では -top 一つで足りることがほとんどなので、まずは scroll-padding-top か scroll-margin-top を覚えておけば十分です。
scroll-snap と組み合わせて止め位置を整える
scroll-padding と scroll-margin は、もともと scroll-snap(スクロールが決まった位置でピタッと吸着する仕組み)とセットで設計されたプロパティです。scroll-snap-type を使ったカルーセルやフルスクリーンのスライドでも、各スライドがスナップして止まる位置を、この2つで微調整できます。
たとえば横スクロールのカルーセルで、左端にカテゴリ表示などの固定の帯がある場合、scroll-padding-left をコンテナに指定すると、スライドがその帯に隠れない位置でスナップします。縦方向のスライドで上部にヘッダーがあるなら scroll-padding-top を使う、という具合です。アンカーリンクの位置ズレ対策も、スナップ位置の調整も、「スクロールが止まる基準位置をずらす」という同じ仕組みの応用だと理解しておくと、両方を無理なく使い分けられます。scroll-snap 自体の使い方は別記事で詳しく解説しています。
余白が効かないときに確認すること
scroll-padding を付ける要素を間違えている
scroll-padding は実際にスクロールしている要素に指定しないと効きません。ページ全体がスクロールしているなら html に、特定のボックス内でスクロールしているならその overflow: auto(や scroll)を持つ要素に付けます。見出しなどのジャンプ先に scroll-padding を付けても効果はないので、その場合は scroll-margin を使う、と切り分けて考えると混乱しません。
ジャンプ先が入れ子のスクロール領域にある
ジャンプ先の要素が、overflow を持つ別のコンテナの中にあると、ブラウザはページ全体だけでなくその内側のコンテナもスクロールさせようとします。html に scroll-padding-top を付けたのに効かないときは、ジャンプ先が入れ子のスクロール領域に入っていないかを確認し、必要ならそのコンテナ側にも余白を指定します。要素ごとに確実に効かせたい場合は、ジャンプ先に scroll-margin-top を直接付けるのが手堅い方法です。
ヘッダーの高さと余白の値が合っていない
余白の値が固定ヘッダーの実際の高さより小さいと、見出しがまだ少しヘッダーに隠れます。反対に大きすぎると、見出しの上に不自然な空きができます。ヘッダーの高さが画面幅で変わるレスポンシブなサイトでは、値を固定の px にしていると、ある画面幅でズレることがあります。ヘッダーの高さを --header-height のような CSS 変数で管理し、scroll-padding-top: var(--header-height); のように参照すると、高さの変更に追従しやすくなります。
まとめ
アンカーリンクで見出しが固定ヘッダーに隠れる位置ズレは、scroll-margin-top か scroll-padding-top でヘッダーの高さぶんの余白を作れば直せます。ジャンプ先の要素側に付けるのが scroll-margin、スクロールするコンテナ側に付けるのが scroll-padding という違いを押さえておきましょう。ページ全体で一律に避けたいなら html に scroll-padding-top、特定の要素だけ調整したいなら要素に scroll-margin-top、と使い分けるのがおすすめです。どちらも scroll-snap のスナップ位置の調整にも使える、スクロールの止め位置をずらすための共通の仕組みです。