アンカーがヘッダーに隠れる:scroll-padding-top を一箇所に
固定ヘッダーはアンカーの着地点を隠す。見出しごとの scroll-margin は必ず漏れる。正解はスクロールコンテナの scroll-padding-top で、実際のヘッダー高さを ResizeObserver から戻す。
固定ヘッダーはアンカーの着地点を飲み込む。目次の見出しをクリックするか、# 付きの共有リンクを開くと、見出しがちょうどヘッダーの下に着地する。
素直な直し方と、その漏れ方
最初に思いつくのは、すべての見出しに scroll-margin-top を付けることです。
h2, h3 { scroll-margin-top: 72px; }
動きますし、レビューも通ります。その後で誰かが脚注を足し、小さなラベルに id を付け、details を開いたときだけ現れるアンカーを足す。オフセットは定数のままで対象だけが増えるので、一つ漏らせば元に戻ります。
正しい置き場所はスクロールコンテナ
scroll-padding-top はスクロールする要素に書き、その中のアンカーをまとめて面倒みます。
html {
scroll-padding-top: var(--anchor-offset);
}
--anchor-offset の既定値はヘッダーの高さ + 余白 1 段分。デスクトップなら足ります。スマートフォンでは足りません。
定数には分からない半分
狭い画面ではナビが折り返し、ヘッダーが 1 行から 2 行になり、実際の高さは --header-height より 1 行分高くなります。CSS 側からは分からないので、書き込んだトークンはすぐ不足し、見出しは再びヘッダーの下へ潜ります。
CSS には計算できずブラウザには測れる値は、ブラウザ側の小さなスクリプト(src/lib/anchor.ts)に任せています。ResizeObserver でヘッダーの getBoundingClientRect().height を測り、余白 1 段を足して --anchor-offset に書き戻す。フォントの読み込み、ウィンドウのリサイズ、ナビの折り返し —— 寸法が変わるたびに測り直します。
JavaScript でスクロールしない理由
window.scrollTo で目標位置を自分で計算することもできます。その場合、滑らかなスクロール、スクロール中に画像が読み込まれて起きるレイアウトずれ、クリック処理を通らないキーボードの Tab 移動という 3 つも自分で抱えることになります。ブラウザはどれも正しく処理済みです。作り直せば境界条件が 3 つ増えるだけです。
レイアウトの知識はレイアウトに置く。JavaScript は CSS が測れない 1 つの数値だけを渡す。
代償はインライン化できない小さなスクリプトです。ResizeObserver が無い環境では既定のトークンに静かに戻ります。アンカーは使えたままで、補正だけが定数に戻るだけです。

コメント
…