2026.09.23 泊まれる場所4,223 立ち寄りスポット59,693 営業中を確認したキャンプ場1,005 note を読む

固定ヘッダーでページ内リンクがずれる原因|見出しにscroll-margin-topを1行足せば隠れない

目次のリンクを押すと、飛んだ先の見出しが固定ヘッダーの裏に隠れる。
直し方は scroll-margin-top か scroll-padding-top を1行足すだけ。
どちらに書くかの決め方と、効かないときに見る場所をまとめた。

固定ヘッダーのあるページで目次のリンクを押すと、飛んだ先の見出しがヘッダーの裏に隠れる。
直し方は、見出しに scroll-margin-top を1行足すか、html に scroll-padding-top を1行足すかのどちらかだ。
どちらも2021年4月から主要なブラウザーで使えるので、JavaScriptで位置を計算し直す必要はもうない。

この記事は、MDNの説明とW3Cの仕様を読んで整理したものだ。
「僕は〜と思う」と書いたところだけが、僕の意見になる。

ずれる原因は、ブラウザーが見出しを画面のいちばん上に合わせるから

ページ内リンク(#見出しのid)を押すと、ブラウザーはその要素の上端が画面の上端にくるようにスクロールする。
ブラウザーにとって、画面の上端はただの上端だ。
そこに position: fixed や position: sticky のヘッダーが乗っていても、気にしてはくれない。

だから見出しは、きちんと画面の上端に来ている。
ただ、その上端をヘッダーがふさいでいるので、見出しだけが隠れて本文の途中から読み始めることになる。
リンクが壊れているわけではなく、止まる位置の基準がヘッダーの分だけずれている、というのが正体だ。

直し方1:見出しに scroll-margin-top を足す

いちばん手軽なのは、飛び先になる要素の側に余白を持たせる方法だ。

h2, h3 {  scroll-margin-top: 80px;}

MDNの説明では、scroll-margin-top は「すべての要素」に指定できるプロパティで、スクロールで合わせるときの領域の上側に、外向きの距離を足すものだ。
80pxと書けば、見出しの80px上が画面の上端に来るように止まる。
ヘッダーの高さが64pxなら、少し余裕を見て72〜80pxにしておくと、見出しの上に一呼吸ぶんの空きができる。

名前に「scroll-snap」の影が見えるので、スクロールスナップ専用の機能だと思われがちだ。
W3Cの仕様(CSS Scroll Snap Module Level 1)では、URLの # で飛ぶとき(:target に当たる要素)や scrollIntoView() で寄せるときも、この余白を含めた領域を画面に入れるように書かれている。
変更履歴には、スナップを使っているかどうかに関係なく scroll-margin を考慮する、とまで書いてある。

直し方2:html に scroll-padding-top を足す

もうひとつは、スクロールする側に「ここは隠れている」と教える方法だ。

html {  scroll-padding-top: 80px;}

MDNの説明では、scroll-padding-top はスクロールコンテナーに指定するプロパティで、固定位置のツールバーなどで隠れている領域を、ものを表示する範囲から外すためにある。
ページ全体のスクロールなら、指定する先は html になる。
見出しを一つずつ探さなくても、1か所で全部の飛び先に効くのがこちらの良さだ。

どちらを使うか

僕は、ヘッダーの高さがページ全体で同じなら html の scroll-padding-top、飛び先ごとに空きを変えたいなら scroll-margin-top で考えている。

  • scroll-padding-top:指定はスクロールする側(ページなら html)。ヘッダーの高さという「ページの事情」を1か所に書ける。
  • scroll-margin-top:指定は飛び先の要素の側。h2だけ広め、表は狭め、のように要素ごとに変えられる。

両方を書くと、仕様の仕組み上、2つの値は足し算で効く。
80pxずつ書いて160pxも空いてしまった、ということになりやすいので、片方に寄せたほうが後で読み返したときに迷わない。

スマホでヘッダーの高さが変わるとき

スマホだけヘッダーが低い、という作りはよくある。
その場合は、ヘッダーの高さをCSS変数に置いて、同じ変数を両方で使うと数字の直し忘れが起きない。

:root { --header_h: 72px; }@media (max-width: 640px) {  :root { --header_h: 56px; }}.site_header { height: var(--header_h); }html { scroll-padding-top: calc(var(--header_h) + 8px); }

CSS変数の考え方は、配色を変えるたびに、同じ色コードを何十箇所も直していた。CSS変数を知って、その作業がなくなったに書いた。
色と同じで、高さも1か所で決めておくと、ヘッダーを作り直したときに飛び先の位置まで一緒に直る。

効かないときに見るところ

  • 指定した先が違う:scroll-padding-top を body に書いても、ページ全体のスクロールには効かないことが多い。ページを動かしているのは html 側だ。
  • スクロールしている箱が別にある:本文だけを overflow: auto の箱でスクロールさせている作りなら、scroll-padding-top はその箱に書く。
  • ヘッダーが sticky で効かない:sticky 自体が効いていない場合は、position:sticky が効かないときに見る場所を先に確かめたい。
  • JavaScriptで位置を足している:昔のやり方で、クリック時にヘッダーの高さを引いてスクロールさせる処理が残っていると、CSSの余白と二重にずれる。

スムーズスクロールを付けるなら

html { scroll-behavior: smooth; } を足すと、ページ内リンクで飛ぶときにするすると動くようになる。
MDNでは、2022年3月からすべてのブラウザーで使えると案内されている。
止まる位置は scroll-margin-top/scroll-padding-top の指定がそのまま効くので、ずれの直し方はさっきの2つと変わらない。

ただ、画面の動きが苦手な人もいる。
僕は付けるなら、OSで「視差効果を減らす」を選んでいる人には止める書き方にしておきたい。

@media (prefers-reduced-motion: no-preference) {  html { scroll-behavior: smooth; }}

まとめ

固定ヘッダーでページ内リンクの飛び先が隠れるのは、ブラウザーが要素を画面の上端に合わせ、その上端をヘッダーがふさいでいるからだ。
ヘッダーの高さが全ページで同じなら html に scroll-padding-top、要素ごとに変えたいなら scroll-margin-top を足す。
JavaScriptで位置を計算していた頃の処理が残っていたら、それを消すところまでやると、ずれが二重にならずに済むと僕は思う。

出典

まる子パパ

まる子パパ

会社員。受託開発のエンジニアを15年以上続けていて、その前は寿司職人・長距離運転手・農業。 技術ブログは、キャンプの合間に踏んだバグと、AI・Web開発の運用の失敗を書いています。 このサイト自体が、開発に携わっているCMS「コンテナ」の稼働中の実例です。 このサイトについて / 小さな自動化の相談

—

ほかにも書いています

受託・note・スタンプ

※受託の相談とnoteは、運営者が個人で受けている・書いているものです。

技術ブログ一覧へ戻る