配了三年浮动层,每次定位都要靠 JS 算坐标——今天 CSS anchor() 把这件事彻底原生化了

配了三年浮动层,每次下拉菜单或 tooltip 的位置你都要写一套 getBoundingClientRect + ResizeObserver + 边界检测的组合拳。77.8M npm 周下载量的 Floating UI/Popper.js 存在的原因只有一个:浏览器从来没有给过你这套原语。2026 年,CSS Anchor Positioning 进了 Baseline Widely Available,Chrome 125+/Firefox 132+/Safari 18.2+,覆盖率 ~92%,这件事彻底变了。

01 这套方案原来有多绕

用 Floating UI 或 Popper.js 做过下拉菜单的都知道,光一个「保证弹窗不超过视口边缘」,就要写三层逻辑:

// 每次打开下拉菜单,手动算
const btn = triggerRef.current;
const menu = menuRef.current;
const viewportHeight = window.innerHeight;
const btnRect = btn.getBoundingClientRect();

// 边界检测
let top = btnRect.bottom + 8;
if (top + menu.offsetHeight > viewportHeight) {
  top = btnRect.top - menu.offsetHeight - 8; // 往上翻
}
menu.style.top = `${top}px`;
menu.style.left = `${btnRect.left}px`;

// 再加 ResizeObserver 监听窗口变化重新计算
// 再加 scroll 监听处理滚动后位置失效

这套逻辑里,getBoundingClientRect 每帧都在跑主线程,ResizeObserver 又是一层观察开销,代码还散落在组件各处——你说它是工程问题,它确实是。

02 CSS Anchor Positioning 三件套

浏览器现在给了你三样东西:anchor-name 给元素起锚点名,position-anchor 让另一个元素绑定到这个锚点,position-areaanchor() 告诉浏览器怎么放这个元素。

第一步:声明锚点

.trigger-btn {
  anchor-name: --my-dropdown;
}

就一行。任何元素都可以是锚点——按钮、图片、甚至是文字片段。

第二步:绑定并定位

.dropdown {
  position: absolute;
  position-anchor: --my-dropdown;
  /* 放在锚点下方居中 */
  position-area: block-end center;
}

position-area 把锚点周围分成 3×3 的网格,block-end center 就是「下方中间」——不需要算坐标,浏览器自己知道怎么放。

第三步:防溢出自动翻转

,这才是本文的核心——原来那三层 JS 逻辑,现在一行 CSS 就能替代:

.dropdown {
  position: absolute;
  position-anchor: --my-dropdown;
  position-area: block-end center;
  /* 如果下方放不下,自动往上翻 */
  position-try-fallbacks: flip-block;
}

flip-block 的意思是:如果默认位置溢出视口,就翻转到对侧。浏览器会自动判断,不需要任何 JS 计算。

03 完整示例:带翻转的下拉菜单

<button class="menu-trigger" popovertarget="my-menu">Options ▾</button>
<div id="my-menu" popover>
  <a href="#">Edit</a>
  <a href="#">Delete</a>
  <a href="#">Duplicate</a>
</div>
.menu-trigger {
  anchor-name: --menu-trigger;
}

[popover] {
  position: absolute;
  position-anchor: --menu-trigger;
  position-area: block-end span-inline;
  min-width: anchor-size(inline); /* 宽度自动等于按钮宽度 */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  padding: 0.5rem;
  inset: unset; /* 清除所有 inset 让 position-area 全权接管 */
}

anchor-size(inline) 读取锚点的宽度赋给下拉菜单——这行替代了过去 getBoundingClientRect().width 的 JS 代码。

[popover]inset: unset 是很多人踩的坑:不清除默认 inset,浏览器可能会用 top: 0; left: 0 覆盖 position-area 的逻辑。

04 anchor() 函数:精细控制

position-area 是声明式的网格定位,适合 80% 的场景。如果你要精确到像素级控制,anchor() 函数直接读锚点的边缘坐标:

.tooltip {
  position: absolute;
  position-anchor: --help-btn;
  /* 精准对齐:tooltip 顶部贴着按钮底部,再留 8px 空隙 */
  top: calc(anchor(bottom) + 8px);
  left: anchor(center);
  translate: -50% 0; /* 水平居中 */
}

这行 top: calc(anchor(bottom) + 8px) 精确等价于原来 JS 里 top: btnRect.bottom + 8

anchor() 还可以直接引用命名锚点:

.tooltip {
  /* 引用另一个元素的锚点 */
  top: calc(anchor(--other-element bottom) + 12px);
}

05 position-visibility:滚动时自动消失

原来另一个常见痛点:锚点滚动出视口后,下拉菜单还悬在原地。position-visibility 让浏览器自己处理这个:

.dropdown {
  position-anchor: --menu-trigger;
  position-area: block-end center;
  /* 当锚点滚出视口或被裁剪时,自动隐藏菜单 */
  position-visibility: anchor-truncation;
}

anchor-truncation 是强需求——没有它,锚点滚动时菜单会「飘」在原位,非常违和。

06 浏览器支持现状(2026 年 9 月)

特性 Chrome/Edge Firefox Safari
anchor-name / position-anchor 125+ 132+ 18.2+
position-area 125+ 132+ 18.2+
anchor() / anchor-size() 125+ 132+ 18.2+
position-try-fallbacks 125+ 147+ 18.2+
position-visibility 125+

全局覆盖率约 92%,Firefox 在 position-visibility 上缺席,但核心定位 API 已全部覆盖。

07 什么场景继续用 Floating UI

CSS Anchor Positioning 覆盖了常见的 tooltip、下拉菜单、浮层定位,但三个场景 Floating UI 仍有价值:

动态列表项:当锚点元素动态插入删除时(虚拟列表、React Window),浏览器可能丢失锚点关联。Floating UI 的 DOM 观察更主动。

多锚点逻辑:CSS 目前只支持每个元素一个主锚点。如果你的 tooltip 要根据三个不同元素的状态决定位置,需要 JavaScript 介入。

Legacy 浏览器兜底:如果项目还要支持 Chrome < 125 或 Firefox < 132,用 @supports (anchor-name: --x) 做渐进增强,保留 Floating UI 作为 fallback。

08 三步下一步

第一步(5 分钟):
在你现有的 tooltip 组件里,加一行 anchor-name: --my-tooltip,然后把 JS 里的 getBoundingClientRect 算坐标那几行注释掉,改成 position: absolute; position-anchor: --my-tooltip; position-area: block-end center;。看效果是否一样。

第二步(10 分钟):
position-try-fallbacks: flip-block 到 tooltip 的 CSS 里,删掉原来 JS 里的手动边界翻转逻辑。刷新页面测试窗口缩放——浏览器自动处理了。

第三步(20 分钟):
Popover API + Anchor Positioning 组合跑通:用 <div popover> 替代 JS 控制的 display: none/ block 切换,用 position-area + position-try-fallbacks 替代整个 Floating UI 实例。项目里 npm uninstall @floating-ui/dom 的时刻就到了。


Floating UI 的 77.8M 周下载量,是前端社区多年积累的对浏览器能力缺失的工程补偿。现在这套补偿机制可以退休了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 12 阅读