锚点跳转需href带#且id严格匹配,sticky侧边栏应置于body子级,滚动高亮须观察section容器并设threshold,平滑滚动用scroll-margin-top修正偏移,三者协同防脱节。

目录项 href 必须带 # 才能触发锚点跳转
点击目录没反应,90% 是因为 href 写成了 href="section1",而不是 href="#section1"。浏览器只识别以 # 开头的链接才执行原生锚点滚动。如果用了 JavaScript 拦截点击,又没手动调用 scrollIntoView(),也会表现为“点了不动”。
确保每个目标元素(比如 <section id="section1"></section>)的 id 值和 href 中的锚名**完全一致**:区分大小写、不含空格、不带 HTML 标签。Markdown 渲染器(如 remark)生成的标题若含 <code>foo,得先用 textContent 提取纯文本再生成 ID,否则目录项会显示为 <code>foo。
侧边栏用 position: sticky 很容易失效
position: sticky 看似省事,但实际依赖父容器的渲染上下文:只要父级有 overflow: hidden、transform、或某些 flex/grid 布局,它就直接退化成 static,目录跟着滚走。
- 最稳妥的做法是把侧边栏放在
直接子级,和<header></header>、<main></main>并列,避免被局部overflow截断 - 用
scroll事件 +getBoundingClientRect()手动控制定位,配合requestAnimationFrame节流,行为完全可控 - 判断是否该“吸顶”的依据不是固定像素值,而是
rect.top —— 即元素顶部已滑出视口上方
滚动高亮必须监听 section 容器,不是 h2 标签
直接用 IntersectionObserver 观察 <h2></h2> 元素会高频误判:标题本身高度小(常仅 20–30px),刚进视口就触发,用户根本还没看到正文;且 Markdown 渲染后标题常被包裹在 <div class="markdown"> 里,观察目标错位。<p>✅ 正确做法是:</p>
<ul>
<li>把每章内容包进 <code><section id="sec-intro"></section> 这类容器,并加 data-toc-id="sec-intro" 便于关联
IntersectionObserver 观察这些 <section></section>,设 threshold: [0.6],即可见比例 ≥ 60% 才算“当前章节”entry.isIntersecting,补一句 const rect = entry.target.getBoundingClientRect(); if (rect.top 做边界校验,抗缩放、字体加载延迟等干扰
平滑滚动和锚点偏移用 scroll-margin-top 解决
纯 CSS 方案最轻量:scroll-behavior: smooth 加 scroll-margin-top 就够了,不用 JS 拼 offset 参数。
- 在
或上加scroll-behavior: smooth - 给每个锚点目标(如
<section id="section1"></section>)加scroll-margin-top: 60px,让滚动停止位置自动上移 60px,避开固定头部遮挡 - 注意:
scroll-margin-top必须设在目标元素上,设在父容器或伪元素上无效 - IE 不支持,只能退回到
element.scrollIntoView({ behavior: 'smooth' }),但 Safari 旧版本不支持behavior参数,得做兼容判断
真正麻烦的从来不是“怎么生成目录”,而是“怎么让它不脱节”——正文删标题、JS 动态插入内容、页面缩放、字体加载延迟……这些都会让目录瞬间失联。MutationObserver 监听 DOM 变动 + IntersectionObserver 观察容器可见性 + scroll-margin-top 修正偏移,三者缺一不可。单独用任何一种,都撑不过三天真实使用。











