锚点跳转被页头吃掉:一次 scroll-padding-top 修全站
吸顶页头会盖住锚点目标,逐个标题写 scroll-margin 迟早漏。正确的位置是滚动容器上的 scroll-padding-top,再用 ResizeObserver 把真实页头高度写回令牌。
吸顶页头会把锚点目标盖住:点目录里的某一节,或者打开一个带 # 的分享链接,标题正好停在页头底下。
错的做法:给每个标题写 scroll-margin
最直觉的修法是给标题加 scroll-margin-top:
h2, h3 { scroll-margin-top: 72px; }
它能用,也能过审。直到有人加了一个脚注、一个挂在小标题上的 id、或者一个展开后才出现的锚点 —— 参数是常量,目标却一直在增加,漏一个就复发。
正确的位置:滚动容器
scroll-padding-top 写在滚动容器上,一次把该容器里所有后代锚点都算进去:
html {
scroll-padding-top: var(--anchor-offset);
}
--anchor-offset 的默认值是「页头高度 + 一档留白」,桌面下够用。手机不够。
常量对不上的那一半
窄屏下导航换行,页头从一行变成两行,真实高度比 --header-height 多出一个行高。CSS 里没人知道这件事,写死的令牌立刻偏低,标题又钻回页头下面。
这种「CSS 算不出来、浏览器量得出来」的数,本站统一交给一个浏览器侧小脚本(src/lib/anchor.ts):用 ResizeObserver 量页头的 getBoundingClientRect().height,加一档留白写回 --anchor-offset。字体加载完、窗口缩放、导航换行,只要尺寸一变就重写一次。
为什么不直接 JS 滚动
拿 window.scrollTo 自己算目标位置也做得到,但要自己处理另外三件事:平滑滚动的动画、滚动途中图片加载造成的布局位移、以及键盘 Tab 跳转不经过你的点击处理函数。这三件浏览器本来就做对了,重做一遍只会多三种边界情况。
布局知识留在布局里,JS 只补 CSS 量不出来的那一个数。
代价是一个内联不了的小脚本。ResizeObserver 不可用时它直接退回默认令牌,不报错、不留白 —— 锚点仍然是可用的,只是补偿回到常量。

评论
…