弹层跟着锚点跑,滚动时却跟丢了——这个坑我踩了三年,今天 CSS 自己会追了

做弹层的时候,最爽的就是把它绑在一个锚点上,然后不用管它的坐标——浏览器自己会算。但有一个场景一直是个坑:锚点本身在一个会滚的容器里,弹层却在容器外面,滚动的时候弹层就「跟丢了」。

这个问题我踩了三年,一直靠 JavaScript 监听 scroll 事件硬算。今天 Chrome 125+ 把 anchor-scroll 这个属性做进去了,这件事彻底原生化了。

问题的根子在哪

锚点定位的工作方式是这样的:浏览器在布局阶段算好锚点和弹层的位置,然后弹层就「定」在那里。但滚动这件事发生在另一个线程上——浏览器的合成线程。布局线程不知道滚动滚到哪了,所以弹层就停在原地,跟锚点彻底错开了。

以前要解决这个问题,只有两条路:要么把弹层塞进滚动容器里(结构就乱了),要么写一个 scroll 监听器实时重算(性能开销大,大页面里几十个弹层一起算,帧率直接掉)。

anchor-scroll 怎么用

用法很简单,在弹层上加一行:

/* 锚点:在一个可滚动的容器里 */
.list-item {
  anchor-name: --my-item;
}

/* 弹层:在滚动容器外面,贴着锚点 */
.popup {
  position: absolute;
  position-anchor: --my-item;
  top: anchor(bottom);
  /* 关键:告诉弹层去跟踪哪个锚点的滚动 */
  anchor-scroll: --my-item;
}

加了这行之后,弹层就会跟踪它指定的锚点——无论锚点滚到哪里,弹层都会跟着追过去,全程不需要一行 JavaScript。

完整的 Demo 场景

看这个场景:一个聊天列表,消息在滚动容器里,每条消息带一个hover才显示的弹出菜单:

<div class="chat-container">
  <!-- 消息列表 -->
  <div class="message-list">
    <div class="message" anchor-name="--msg-1">
      <span>你好</span>
      <button class="action-btn">⋮</button>
    </div>
    <!-- 更多消息 -->
  </div>

  <!-- 弹层在容器外面,跟随锚点滚动 -->
  <div class="popup-menu" role="menu">
    <button role="menuitem">回复</button>
    <button role="menuitem">复制</button>
    <button role="menuitem">删除</button>
  </div>
</div>
.message-list {
  height: 400px;
  overflow-y: auto;
}

.message {
  anchor-name: --msg-1;
  /* anchor-name 是动态的,这里示意 */
}

.popup-menu {
  position: fixed;  /* 或 absolute */
  position-anchor: --msg-1;  /* 跟踪哪个锚点 */
  top: anchor(bottom);
  left: anchor(right);
  anchor-scroll: --msg-1;  /* 关键:跨容器跟踪滚动 */
}

滚动列表的时候,弹层会跟着对应的消息走。

注意事项

目前这个属性有几个限制:

1. 浏览器兼容性anchor-scroll 在 Chrome 125+ / Edge 125+ 可用,Firefox 和 Safari 还未支持。如果要上线,需要用 @supports 做降级处理:

.popup-menu {
  position: fixed;
  position-anchor: --msg-1;
  top: anchor(bottom);
  left: anchor(right);
}

@supports (anchor-scroll: --msg-1) {
  .popup-menu {
    anchor-scroll: --msg-1;
  }
}

2. 锚点滚出视口了弹层不会自动消失:弹层会一直追着锚点,即使锚点已经滚到了滚动区域的外部。这是当前版本的能力边界,需要配合 IntersectionObserver 来处理显隐。

3. 只支持单个锚点:一个弹层一次只能跟踪一个锚点。如果需要动态切换锚点,需要在 JavaScript 里动态更新 anchor-scroll 的值。

和 JavaScript 方案比性能差在哪

用 Floating UI / Popper.js 做的话,每次滚动都要:

// 这些全在主线程跑
container.addEventListener(scroll, updatePosition, { passive: true });
new ResizeObserver(() => updatePosition()).observe(container);
// 几十个弹层 × 每次滚动都重算 = 帧率杀手

anchor-scroll 的逻辑跑在浏览器的布局引擎里,不是主线程。100 个弹层同时跟踪锚点,比 100 个 JavaScript scroll 监听器的开销要低很多。

总结

如果你现在就在用锚点定位做弹层、tooltip、下拉菜单,而且锚点在滚动容器里——加上 anchor-scroll 这一行,就能把这些场景彻底交给 CSS,删掉对应的 JavaScript 逻辑。建议先在开发环境测,兼容性没问题了再上生产。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 13 阅读