固定ヘッダーでページ内リンクがずれる原因|見出しに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で位置を計算していた頃の処理が残っていたら、それを消すところまでやると、ずれが二重にならずに済むと僕は思う。
出典
ほかにも書いています
受託・note・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
