弹层跟着锚点跑,滚动时却跟丢了——这个坑我踩了三年,今天 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 逻辑。建议先在开发环境测,兼容性没问题了再上生产。
评论区
登录后可评论。