動画で読む
まず結論 — 固定ヘッダーで隠れる問題は scroll-padding を第一選択にする
固定ヘッダーでアンカー先が隠れる問題は、原則としてスクロールコンテナ側の scroll-padding-block-start で解決します。要素ごとに個別の余白がほしいときだけ、例外として scroll-margin を要素側に足す。この順番を守ると、マウスでもキーボードでもスクロール位置が破綻しません。
きっかけは Zenn の scroll-margin と scroll-padding の使い分け という記事でした。ページ内リンクで見出しに飛ぶと、その見出しがサイト上部に貼りついたヘッダーの裏に隠れて読めない。よくある不具合です。私も以前、目次(Table of Contents、ページ内の見出し一覧)から本文へジャンプする機能を付けたとき、まさにこれをやらかしました。飛んだ先の h2 がヘッダーにちょうど食われて、読者には「リンクが効いていない」ように見えてしまう。
この 2 つのプロパティは名前が似ているせいで混同されがちですが、効く相手が違います。そこを最初に分けて考えると、どちらをいつ使うかは自然に決まります。
二つのプロパティの責務 — コンテナに効くか、要素に効くか
scroll-padding はスクロールコンテナ(多くの場合 html / :root)に効き、scroll-margin はスクロール先の要素(見出しなど)に効きます。前者は「ビューポートの内側にどれだけ安全地帯を作るか」、後者は「この要素の周りにどれだけ余白を持たせるか」を決める、という分担です。
コンテナ側で一括して逃がすなら、こう書きます。
:root {
--header-height: 64px;
scroll-padding-block-start: var(--header-height);
}
scroll-padding-block-start は論理プロパティ(書字方向に追従するプロパティ)で、横書きでは上端に対応します。これでページ内アンカーに飛んだとき、コンテナはヘッダー分だけ手前で止まり、見出しがヘッダーの下にちゃんと現れます。
要素側で逃がすなら、対象の見出しにこう足します。
:target {
scroll-margin-block-start: var(--header-height);
}
:target は URL のフラグメント(#section-2 の # 以降)に一致する要素を指す擬似クラスです。scroll-margin はその要素の上に余白を確保し、スクロールが止まる位置をヘッダー分だけ押し下げます。見た目の結果はコンテナ側の指定とほぼ同じになります。違いは次の章で出ます。
なぜ scroll-padding を第一選択にするか — キーボード操作で差が出る
見た目が同じなら、私が scroll-padding を先に選ぶ理由はキーボード操作です。scroll-margin はアンカーで飛んだ瞬間の停止位置しか補正しませんが、scroll-padding はコンテナのスクロール量そのものを補正するため、スペースキーや Page Down での 1 画面送りにも効きます。
具体的に何が起きるか。scroll-margin だけを当てた状態でスペースキーを押すと、ブラウザはビューポートの高さぶんだけ素直にスクロールします。このとき固定ヘッダーの高さは計算に入りません。結果、1 画面送るたびに、上端のコンテンツがヘッダーの裏に少しずつ食われていきます。マウスを使わずキーボードだけで読む人にとっては、行が毎回欠けて見える、地味に厄介な体験です。
一方 scroll-padding は、コンテナの「最適な表示領域」を内側に狭める指定です。だからスペースキーの 1 画面送りでも、ヘッダー高さを差し引いた量だけスクロールしてくれます。読者がどの方法でスクロールしても、ヘッダーの裏に潜る行が出ません。アクセシビリティ(誰にとっても使えること)の観点でも、こちらが筋がいい。リンクで飛ぶ人だけでなく、キーボードで読む人まで含めて面倒を見てくれるからです。状態やテーマをまたいで破綻させない発想は、以前書いた状態とテーマを横断した contrast 監査とも地続きでした。
scroll-margin の出番 — scroll-snap と個別要素の微調整
では scroll-margin は不要かというと、そんなことはありません。出番は「コンテナ全体ではなく、特定の要素だけ止まる位置をずらしたい」ときと、scroll-snap(スクロールが特定位置に吸着する仕組み)で各スライドの吸着位置を要素ごとに調整したいときです。
たとえば横スクロールのカードギャラリーで、各カードを左端にぴたっと吸着させつつ、最初の 1 枚だけ少し余白を持たせたい。こういう要素単位の例外は、コンテナの scroll-padding では表現しきれません。scroll-snap-align と組み合わせて、カード側に scroll-margin を当てるのが素直です。
.gallery {
scroll-snap-type: x mandatory;
}
.gallery > .card {
scroll-snap-align: start;
}
.gallery > .card:first-child {
scroll-margin-inline-start: 24px;
}
つまり、全体の基準はコンテナの scroll-padding で引いておき、そこから外れる個別の要素だけ scroll-margin で上書きする。基準と例外という関係で捉えると、二者択一ではなく重ねて使う道具だと分かります。読み出し位置を意図通りに揃えるという点では、以前のピクセルパーフェクトを意図の一致として捉え直す話と同じ考え方です。
実装の落とし穴 — 高さの SSoT と smooth scroll
実装でつまずくのは、ほぼ「ヘッダー高さの数値があちこちに散らばる」ことと「スムーズスクロールとの相性」の 2 点です。先に対策を言うと、高さは CSS 変数 1 個に集約し、スクロール挙動はコンテナ側にまとめます。
私が最初にやった失敗は、scroll-padding-block-start: 64px とハードコードしたことでした。後日ヘッダーにお知らせバナーを足して高さが 96px になったとき、アンカー位置だけ 64px のまま取り残され、見出しが 32px ぶんヘッダーに食われた。原因に気づくまで 15 分ほど DevTools とにらめっこしました(過去の私がコメント 1 行残してくれていれば、と毎回思います)。ヘッダー高さは --header-height のような変数 1 個を SSoT(Single Source of Truth、唯一の出どころ)にして、ヘッダーの height も scroll-padding-block-start も同じ変数を参照させる。これだけで取り残しは起きなくなります。
もう 1 つはスムーズスクロールです。html { scroll-behavior: smooth; } を入れると、アンカー移動がぬるっと動いて気持ちいいのですが、scroll-padding とは独立した設定なので、両方をコンテナ側にまとめて書くと管理が楽になります。なお scroll-padding は 2021 年 4 月時点で主要ブラウザに行き渡っていて(Baseline widely available)、2026 年の今は対応を気にせず使えます。注意点があるとすれば、scroll-margin / scroll-padding はあくまでスクロール停止位置の調整であって、固定ヘッダー自体のレイアウトや重なり順(z-index)とは別の話だ、という切り分けくらいです。
持ち帰り — まず scroll-padding、例外だけ scroll-margin
最後に判断の順番を整理します。固定ヘッダーでアンカーが隠れる問題に出会ったら、まずコンテナに scroll-padding-block-start を引く。それで足りない個別の要素だけ scroll-margin で上書きする。この 2 段で、ほとんどのケースは数行で片付きます。
高さは CSS 変数 1 個に集約し、ヘッダーの実高とアンカー補正値を同じ出どころから引く。scroll-snap を使うギャラリーで要素ごとに吸着位置を変えたいときだけ、scroll-margin を要素側へ。判断材料は「コンテナ全体の話か、特定要素だけの話か」のひとつだけです。この記事も AI が下書きを書き、私が動作を確認・編集して公開しています。名前が紛らわしい 2 プロパティでも、効く相手で割り切れば、迷う場面はほとんどなくなりました。
よくある質問
- scroll-margin と scroll-padding はどちらを使えばよいですか?
- 原則はコンテナ側の scroll-padding を第一選択にします。ページ全体のアンカー位置を一括で補正でき、キーボードの 1 画面送りにも効くためです。特定の要素だけ止まる位置をずらしたいときや scroll-snap の吸着位置を調整したいときに、例外として要素側の scroll-margin を足します。
- scroll-margin だけだと何が問題になりますか?
- scroll-margin はアンカーで飛んだ瞬間の停止位置しか補正しません。スペースキーや Page Down での 1 画面送りには効かないため、キーボードで読み進めると上端のコンテンツが固定ヘッダーの裏に少しずつ隠れていきます。scroll-padding ならスクロール量そのものを補正するのでこの問題が起きません。
- ヘッダーの高さが変わるとアンカー位置がずれます。どう防ぎますか?
- ヘッダー高さを --header-height のような CSS 変数 1 個に集約し、ヘッダーの height と scroll-padding-block-start の両方を同じ変数から参照させます。値をハードコードすると、後日バナーを足して高さが変わったときにアンカー補正値だけ取り残され、見出しがヘッダーに食われます。