
Tooltip、下拉菜单、气泡确认——这些东西在前端里写了十几年,绝大多数项目仍在用 Floating UI / Popper 一类库算坐标。不是库不好,是浏览器以前不给你原生锚点。CSS Anchor Positioning 现在已经进入可认真评估的阶段:Chrome 完整实现较早,Firefox / Safari 在跟进,基础属性在逐步进入 Baseline 讨论区。问题从「能不能用」变成「哪些场景该先切」。
我这边的结论很务实:新做的轻量浮层可以先上 CSS;复杂边界、虚拟列表、跨 shadow root 的遗留组件,先别急着拆库。
它解决的是哪一类痛
浮层的核心矛盾一直是两件事:
- 相对谁定位(trigger / anchor)
- 空间不够时怎么翻面、怎么避让(flip / shift)
以前你要监听 resize、scroll,量 getBoundingClientRect,再写 top/left,还要处理祖先 overflow、transform 建立的 containing block。逻辑不难,但脏:滚动容器一变,bug 就回来。
Anchor Positioning 把「锚」变成一等公民:
.Trigger {
anchor-name: --menu-btn;
}
.Menu {
position: absolute;
position-anchor: --menu-btn;
bottom: anchor(top);
left: anchor(left);
/* 空间不够时尝试另一套位置 */
position-try-fallbacks: flip-block, flip-inline;
}
坐标计算交给布局引擎。滚动时浮层跟着锚点走,不必再挂一串被动监听。对 tooltip、select 面板、表格行操作菜单,这类「短生命周期、单锚点」组件,收益很直接。
和 Floating UI 的边界
别神话原生 API。库仍然更擅长这些:
- 虚拟列表里 anchor 频繁挂载卸载
- 多锚点、动态 portal 到
document.body且要跨多层 stacking context - 需要 JS 才能知道的业务约束(「宁可压住侧栏,也不能挡住主 CTA」)
- 旧浏览器与 polyfill 策略尚未谈拢的团队
我现在的拆法是:
| 场景 | 建议 |
|---|---|
| 表单字段旁的帮助气泡 | CSS anchor 优先 |
| 导航里的简单下拉 | CSS anchor + 少量 JS 管开关 |
| 画布 / 无限表格上的上下文菜单 | 继续 Floating UI |
| 设计系统里已经稳定的 Popover | 别为了新而新,等跨浏览器基线再迁 |
迁移成本主要不在语法,而在「谁负责打开关闭、焦点陷阱、Esc、点击外部」。这些仍是 JS 的活。CSS 只接手定位。
落地时容易踩的坑
命名冲突。 anchor-name 是全局的(在 tree 作用域语义下仍要小心复用)。列表渲染时别写死同一个 --tip,用唯一定位或按实例生成。
position-try 不是万能翻面。 它解决常见的上下左右翻转,复杂避让(沿曲线、贴齐某条安全边)仍可能要 JS 补一刀。
和 popover / top-layer 叠用。 原生 popover 属性解决的是顶层显示与 light dismiss,anchor 解决的是相对位置。两者很搭,但测试时要单独看焦点顺序和 ::backdrop。
Scroll 容器。 锚点在可滚动祖先内时,确认浮层是挂在同一 containing block 策略下,还是你有意 portal 出去。portal 出去后,anchor 关系仍然有效,但层叠和裁剪规则要重新看一遍。
减少动效。 浮层若带过渡,继续尊重 prefers-reduced-motion。定位稳了不等于可以加炫的位移。
一个最小可用模式
交互仍用一点 JS,定位尽量交给 CSS:
<button class="btn" style="anchor-name: --save-help">保存</button>
<div id="tip" class="tip" popover>
保存不会自动提交审核
</div>
.tip {
position: absolute;
position-anchor: --save-help;
bottom: calc(anchor(top) + 8px);
left: anchor(center);
translate: -50% 0;
position-try-fallbacks: flip-block;
}
const btn = document.querySelector('.btn');
const tip = document.querySelector('#tip');
btn.addEventListener('click', () => {
tip.togglePopover();
});
React 里同理:开关状态放 state,坐标别再塞进 useEffect。若你还在为每次 scroll 调 computePosition,这就是该删的那一层。
图:锚点与浮层的关系——布局引擎直接认领相对位置

工程上怎么推进
- 盘点:设计系统里有多少 Popover/Tooltip 只做「点锚 + 翻面」。
- 建对照页:同一组件 CSS 版 vs Floating UI 版,测 scroll 父级、zoom、移动端键盘顶起。
- 定浏览器门槛:内部工具可以更激进;对外产品要写清楚 fallback(要么降级为库,要么简化为静态说明)。
- 别和动画一起大改:先稳定位,再谈 view transition 或进入退出动画。
浏览器把坐标算走,前端从「模拟布局」退回「描述关系」。这才是平台该干的事。Floating UI 不会立刻消失,但新代码里,默认先问一句:这个浮层,真的还需要自己量盒子吗?
小结
CSS Anchor Positioning 适合单锚点、短生命周期、避让规则不极端的浮层。它砍掉的是 resize/scroll 上的坐标同步,不是无障与状态机。新组件优先原生;重型表面继续用成熟库。把「相对谁」写进 CSS,把「何时出现」留在 JS,边界清楚了,代码会安静很多。


