每次想给 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,迁移路径很清晰:

  1. 从 tooltip 开始:最简单,状态最少,最容易验证正确性
  2. @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 兜底 */
  }
}
  1. 审计你的 bundle:删掉 Floating UI / Popper.js 的 import,测量打包体积变化
  2. 在真实设备上测 INP:特别是低配 Android 机,主线程压力减少的效果最明显

2026 年写 tooltip,下拉菜单,弹出层,思路已经从「引入一个库,然后配置它」变成了「三个 CSS 属性,让浏览器来算」。这是 CSS 继 flexgridcalc 之后,又一次把原本属于 JS 的领域收回去。

你的项目里还有多少浮层定位靠 JS 在算?

评论区

0 条评论

登录后可评论。

阿柯·前端架构 17 阅读