每次想给 tooltip 找个准确位置都要写 30 行 JS——今天 3 行 CSS 把这件事彻底原生化了
你翻过任何一个 UI 组件库的文档,就会发现 tooltip 和下拉菜单的定位是个专门的话题。写个按钮,再写个悬停气泡,两者之间要能「粘住」,还要能翻转到视口边缘——这在 2025 年之前,答案无一例外是:引入一个至少 6KB 的 JS 库,用它的 API 初始化它,用 ResizeObserver 监听它,然后在每次 scroll 事件里祈祷它不要跳帧。
这件事在 2026 年彻底变了。
三个属性,一个函数,零 JS
CSS Anchor Positioning 是 W3C CSSWG 的标准,核心就四件事:anchor-name 给触发元素起名字,position-anchor 让定位元素「认领」这个名字,anchor() 函数读取锚点的边缘坐标,再加一个 @position-try 定义翻转到哪里。
/* 触发器:给它起个名字 */
.trigger {
anchor-name: --my-tooltip;
}
/* 气泡:认领这个名字,然后说「我要贴在它上方」 */
.tooltip {
position: absolute;
position-anchor: --my-tooltip;
bottom: anchor(top); /* 气泡底部贴锚元素顶部 */
left: anchor(center); /* 水平居中 */
translate: -50% 0;
margin-bottom: 8px; /* 间距 */
/* 当上方空间不够时,自动翻到下方 */
position-try-fallbacks: --flip-below;
}
@position-try --flip-below {
bottom: auto;
top: anchor(bottom);
margin-bottom: 0;
margin-top: 8px;
}
这就是全部。没有 createPopper(),没有 update(),没有 ResizeObserver,没有 scroll 监听。浏览器在渲染引擎里直接算好坐标,随滚动、resize、布局变化自动重算。
12KB 的库被删掉了,主线程空出来了
你可能会想:这点活 JS 也不是不能干。但问题在于,Floating UI 的 computePosition() 跑在主线程的微任务队列里,每一次触发重排(reflow)。如果你页面上有 20 个 tooltip、15 个下拉菜单、10 个弹出层,每个交互触发一次定位重算,这些计算就站在渲染流水线里跟你的 CSS 动画、事件处理抢时间。
Mintec 在 Q2 2026 年迁移了三个客户项目,用 CSS Anchor Positioning 替换 Floating UI,测量到含 30+ tooltip 的页面 INP(Interaction to Next Paint)改善了 40–80ms。对用户来说,这不是一个数字,是每次悬停、每次点击的响应感的差距。
另一个直接的账:删掉 Floating UI(约 12KB)+ Popper.js(约 3KB),等于给每个用户省了 15KB 的下载、解析和执行。Time to Interactive 和解析时间都直接受益。
浏览器支持:2026 年夏天三大引擎全部就绪
这是关键的变化节点。CSS Anchor Positioning 2025 年只有 Chrome/Edge 125 支持,Firefox 和 Safari 都还不在列。但 Interop 2026 改变了这一切:
| 浏览器 | 支持版本 | 覆盖率 |
|---|---|---|
| Chrome / Edge | 125+ | 84%+ |
| Firefox | 147+(2026 年 4 月默认开启) | 84%+ |
| Safari | 26+(2025 年 10 月发布) | 84%+ |
| iOS Safari | 26+ | 随 Safari |
84% 的全球覆盖率已经过了生产可用的门槛。Safari 26.4 开始支持 @position-try 翻转行为;Safari 18.2–18.3 支持 anchor() 但不支持 @position-try,此时气泡定位正确但不自动翻转——大多数用户感知不到差异。
对于还在用旧浏览器的用户,社区维护的 polyfill(@oddbird/css-anchor-positioning,约 8KB gzip)按需加载:
if (!CSS.supports('anchor-name: --a')) {
const script = document.createElement('script');
script.src = 'https://unpkg.com/@oddbird/css-anchor-positioning';
document.head.appendChild(script);
}
现代浏览器零额外成本,只有旧浏览器才下载 polyfill。
配合 Popover API:连开关逻辑都免了
tooltip 和下拉菜单有个伴生的需求:显示/隐藏的状态管理——点击打开,点击外部关闭,ESC 关闭,获得焦点的无障碍行为。这些过去也是一个 JS 事件监听器接一个 JS 事件监听器。
HTML Popover API 把这部分也接管了:
<button popovertarget="my-menu" style="anchor-name: --menu-btn">
打开菜单
</button>
<div id="my-menu" popover>
<!-- Popover API 管:点击外部关闭、ESC 关闭、焦点管理 -->
<ul>
<li><a href="/settings">设置</a></li>
<li><a href="/logout">退出</a></li>
</ul>
</div>
#my-menu {
position: absolute;
position-anchor: --menu-btn;
position-area: bottom span-inline-end; /* 定位在锚元素左下 */
}
popover="auto" 是 light-dismiss(点击外部关闭),popover="manual" 只通过按钮关闭。气泡内容层叠在最顶层(top-layer),自动脱离任何 overflow: hidden 的约束。
什么时候继续用 JS 方案
CSS Anchor Positioning 不是银弹。有三类场景目前还是 JS 库的领地:
拖拽中跟随鼠标:anchor 位置在下一帧重算,跟不上鼠标的像素级移动。
跨 iframe 定位:anchor 和目标在不同的 document 里,anchor-name 没法跨边界。
虚拟元素:没有真实 DOM 节点的锚点(比如给 Canvas 上的元素挂 tooltip),anchor-name 需要可见节点。
这三类场景加起来,大概覆盖不到 10% 的浮层需求。剩下的 90%,现在都可以用 CSS 写。
迁移的第一步
如果你现在用 Floating UI 或 Popper.js,迁移路径很清晰:
- 从 tooltip 开始:最简单,状态最少,最容易验证正确性
- 用
@supports包裹新写法,旧写法兜底:
@supports (anchor-name: --test) {
.tooltip {
position: absolute;
position-anchor: --trigger;
bottom: anchor(top);
left: anchor(center);
}
}
@supports not (anchor-name: --test) {
.tooltip {
/* 旧:Floating UI 兜底 */
}
}
- 审计你的 bundle:删掉 Floating UI / Popper.js 的 import,测量打包体积变化
- 在真实设备上测 INP:特别是低配 Android 机,主线程压力减少的效果最明显
2026 年写 tooltip,下拉菜单,弹出层,思路已经从「引入一个库,然后配置它」变成了「三个 CSS 属性,让浏览器来算」。这是 CSS 继 flex、grid、calc 之后,又一次把原本属于 JS 的领域收回去。
你的项目里还有多少浮层定位靠 JS 在算?
评论区
登录后可评论。