弹层跟着锚点走,但锚点滚出视口了弹层还在原地——这个坑我踩了三年,今天 CSS 自己会追了
弹层跟着锚点走,但锚点滚出视口了弹层还在原地——这个坑我踩了三年,今天 CSS 自己会追了。
配过 anchored tooltip 或 popover 的同学,大概都遇到过这个场景:锚点按钮在一个可滚动的容器里,用户滚动了,弹层还钉在原地,因为它不在滚动容器内部,浏览器不知道要跟踪。
这个问题我踩了三年,每次都要靠 JS 监听滚动事件然后手动更新弹层位置——直到 anchor-scroll 属性出现。
anchor-scroll:让弹层追着锚点滚
这个属性的用法非常简单:给弹层设置 anchor-scroll: --my-anchor,浏览器就会自动监听指定锚点的滚动位置,让弹层跟着锚点一起滚动。
.button {
anchor-name: --my-anchor;
}
.tooltip {
position-anchor: --my-anchor;
anchor-scroll: --my-anchor;
}
不需要任何 JS,不需要 scroll 事件监听,不需要 requestAnimationFrame。这行 CSS 做的事情本质上是:让弹层的”锚定坐标系”跟随锚点元素的滚动上下文一起运动。
配合 scroll-driven animations:弹层自己会随滚动”变身”
如果把 anchor-scroll 和 scroll-driven animations 结合起来,还能实现更骚的效果——弹层的尺寸随着锚点滚动而变化。
比如一个”阅读进度胶囊”,它锚定在页面顶部,但宽度要随着用户向下滚动从 0% 逐渐撑开到 100%:
.progress-capsule {
anchor-name: --reading-progress;
anchor-scroll: --reading-progress;
width: calc-size(
anchor-size(--reading-progress, self) *
(scroll(nearest) - 0) / 100,
min
);
}
这个写法的逻辑是:弹层的基准宽度 = 锚点元素自身的宽度(anchor-size(..., self)),然后乘以滚动进度(scroll(nearest) 返回 0~100 的滚动百分比),最后用 calc-size() 让宽度计算结果生效。锚点滚多长,弹层就长多宽。
更直观一点的写法是用 @keyframes + animation-timeline: scroll():
@keyframes capsule-expand {
from { width: 8px; }
to { width: calc(100vw - 32px); }
}
.progress-capsule {
anchor-name: --reading-progress;
anchor-scroll: --reading-progress;
animation: capsule-expand linear both;
animation-timeline: scroll(nearest block);
}
弹层从 8px 宽,随着滚动逐渐撑开到屏宽减去边距,整个过程零 JS。
为什么 JS 方案一直是次优解
传统 JS 方案的问题是:滚动事件发生在主线程,而弹层位置更新也要走主线程。当页面滚动很频繁时,监听器回调和布局计算会相互抢占资源,导致弹层”跟不上”——看起来就是弹层和锚点之间有延迟或跳变。
anchor-scroll 的实现是在浏览器的滚动处理线程里直接更新锚定关系,不经过主线程,所以不存在这个问题。CSS 把它原生化了之后,相当于把滚动和锚定绑定变成了浏览器的内置行为,而不是开发者手写的回调逻辑。
浏览器支持现状
anchor-scroll 是 CSS Anchor Positioning 的一部分,Chrome 125+ 和 Edge 125+ 支持。Firefox 和 Safari 支持中。整体还需要 @supports 降级:
@supports (anchor-scroll: --test) {
.tooltip {
anchor-scroll: --my-anchor;
}
}
怎么判断要不要用
如果你的弹层锚点在一个会滚动的容器里,而你现在的解决方案是 JS 监听滚动事件手动更新位置——这行 CSS 值得一试。它不只是省 JS,关键是解决了 JS 方案里”弹层跟不上滚动”的体验问题。
下一步建议:在本地跑一个锚点 + 弹层的场景,给弹层加上 anchor-scroll,然后快速滚动容器,观察弹层和锚点之间有没有跳变或延迟。如果以前被这个体验问题折磨过,现在就是换方案的时候了。
评论区
登录后可评论。