CSS Anchor:浮层少绑一层 JS
前端约 5 分钟阅读

CSS Anchor:浮层少绑一层 JS

Anchor Positioning 把 tooltip/下拉的坐标交回浏览器。适合单锚点短浮层;复杂表面继续 Floating UI。开关与无障仍是 JS 的活。

cover

Tooltip、下拉菜单、气泡确认——这些东西在前端里写了十几年,绝大多数项目仍在用 Floating UI / Popper 一类库算坐标。不是库不好,是浏览器以前不给你原生锚点。CSS Anchor Positioning 现在已经进入可认真评估的阶段:Chrome 完整实现较早,Firefox / Safari 在跟进,基础属性在逐步进入 Baseline 讨论区。问题从「能不能用」变成「哪些场景该先切」。

我这边的结论很务实:新做的轻量浮层可以先上 CSS;复杂边界、虚拟列表、跨 shadow root 的遗留组件,先别急着拆库。

它解决的是哪一类痛

浮层的核心矛盾一直是两件事:

  1. 相对谁定位(trigger / anchor)
  2. 空间不够时怎么翻面、怎么避让(flip / shift)

以前你要监听 resize、scroll,量 getBoundingClientRect,再写 top/left,还要处理祖先 overflow、transform 建立的 containing block。逻辑不难,但脏:滚动容器一变,bug 就回来。

Anchor Positioning 把「锚」变成一等公民:

css
.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:

html
<button class="btn" style="anchor-name: --save-help">保存</button>
<div id="tip" class="tip" popover>
  保存不会自动提交审核
</div>
css
.tip {
  position: absolute;
  position-anchor: --save-help;
  bottom: calc(anchor(top) + 8px);
  left: anchor(center);
  translate: -50% 0;
  position-try-fallbacks: flip-block;
}
js
const btn = document.querySelector('.btn');
const tip = document.querySelector('#tip');
btn.addEventListener('click', () => {
  tip.togglePopover();
});

React 里同理:开关状态放 state,坐标别再塞进 useEffect。若你还在为每次 scroll 调 computePosition,这就是该删的那一层。

图:锚点与浮层的关系——布局引擎直接认领相对位置

工程上怎么推进

  1. 盘点:设计系统里有多少 Popover/Tooltip 只做「点锚 + 翻面」。
  2. 建对照页:同一组件 CSS 版 vs Floating UI 版,测 scroll 父级、zoom、移动端键盘顶起。
  3. 定浏览器门槛:内部工具可以更激进;对外产品要写清楚 fallback(要么降级为库,要么简化为静态说明)。
  4. 别和动画一起大改:先稳定位,再谈 view transition 或进入退出动画。

浏览器把坐标算走,前端从「模拟布局」退回「描述关系」。这才是平台该干的事。Floating UI 不会立刻消失,但新代码里,默认先问一句:这个浮层,真的还需要自己量盒子吗?

小结

CSS Anchor Positioning 适合单锚点、短生命周期、避让规则不极端的浮层。它砍掉的是 resize/scroll 上的坐标同步,不是无障与状态机。新组件优先原生;重型表面继续用成熟库。把「相对谁」写进 CSS,把「何时出现」留在 JS,边界清楚了,代码会安静很多。

相关文章