配了三年浮动层,每次定位都要靠 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-area 或 anchor() 告诉浏览器怎么放这个元素。
第一步:声明锚点
.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 周下载量,是前端社区多年积累的对浏览器能力缺失的工程补偿。现在这套补偿机制可以退休了。
评论区
登录后可评论。