配了三年弹层组件,今天才发现根子不在定位逻辑——是浏览器终于把这件事还给了 CSS
写过三年弹层组件,你大概知道这个痛:给 Popover 写好了「在按钮下方弹出」的定位,结果按钮刚好在页面顶部——弹层被浏览器边缘截掉一半,最常见的解法是 JS 硬算位置、翻转方向、加 ResizeObserver 监听。
2026 年了,这套逻辑可以全删了。
CSS @position-try 加上锚点定位(CSS Anchor Positioning),把「弹层找路」这件事彻底还给了浏览器原生渲染引擎。Chrome 125、Firefox 131、Safari 18.2 全部上线,今年 1 月进入 Baseline 2026,全球覆盖率约 88%,生产环境已经可以直接用。
弹层定位,以前是怎么做的
传统的弹层定位,核心依赖 getBoundingClientRect() 拿到锚点坐标,然后 JS 算弹层位置、加滚动监听、算窗口边界、手动翻转方向。主流方案是 Popper.js(3 KB)或 Floating UI(12 KB),做的事情本质上是一样的:JS 模拟浏览器本该原生做的事。
这套方案有几个典型坑:滚动时弹层跟丢锚点(除非额外加 scroll 监听);overflow: hidden 容器会切断弹层;ResizeObserver 漏加一个就崩;多个弹层同时存在时代码复杂度急剧上升。
CSS 锚点定位:声明式绑锚点
CSS Anchor Positioning 的核心是两个属性:
/* 触发元素:注册锚点 */
.trigger {
anchor-name: --my-anchor;
}
/* 弹层:绑定锚点并定位 */
.popover {
position: absolute;
position-anchor: --my-anchor;
bottom: anchor(top); /* 弹层底部对齐锚点顶部 = 弹层在锚点上方 */
left: anchor(center); /* 水平居中 */
translate: -50% 0;
margin-bottom: 8px;
}
anchor() 函数读取锚点元素的物理边缘(top/right/bottom/left)或逻辑位置(center/start/end)。弹层和触发元素不需要是父子或兄弟关系——只要锚点先渲染,弹层在任何位置都能绑上去。
@position-try:浏览器自己找备用路
光能绑锚点还不够,真正的价值在于 @position-try——弹层快要溢出视口时,浏览器自己换一条路,而不是靠 JS 来算。
.popover {
position: absolute;
position-anchor: --my-anchor;
bottom: anchor(top);
left: anchor(center);
translate: -50% 0;
/* 定义备用位置列表,浏览器按顺序找第一个不溢出的 */
position-try-fallbacks: --flip-below, --flip-right;
}
@position-try --flip-below {
top: anchor(bottom); /* 翻到锚点下方 */
bottom: unset;
}
@position-try --flip-right {
left: anchor(right); /* 翻到锚点右侧 */
top: anchor(center);
bottom: unset;
translate: 0 -50%;
}
position-try-fallbacks 里的名字对应 @position-try 声明的具名规则。浏览器在渲染时会依次尝试每个备用位置,直到找到一个不溢出差视口的。这和 Popper.js 的 flip() middleware 做的是同一件事,但完全在渲染引擎里跑,不需要任何 JS 计算,不触发重排。
position-area 是另一个更简洁的写法,用 3×3 网格描述弹层位置:
.popover {
position: absolute;
position-anchor: --my-anchor;
position-area: top center; /* 锚点的上方中间区块 */
}
可组合 top left、bottom span-all 等,适合固定方向的弹层,不涉及翻转逻辑时比 @position-try 更直观。
配合 Popover API,连 show/hide 都不用 JS
锚点定位和 HTML Popover API 是天然组合:
<button popovertarget="my-menu" id="menu-trigger">Open</button>
<div popover id="my-menu">选项 1 / 选项 2 / 选项 3</div>
#menu-trigger {
anchor-name: --menu-trigger;
}
[popover] {
position: absolute;
position-anchor: --menu-trigger;
top: calc(anchor(bottom) + 4px);
left: anchor(left);
position-try-fallbacks: --flip-above;
}
@position-try --flip-above {
top: unset;
bottom: calc(anchor(top) + 4px);
}
按钮控制显隐、弹层自动定位、溢出自动翻转——全程零 JS。浏览器自己处理 light-dismiss(点击外部关闭)、accessibility 焦点管理、层叠上下文。
渐进增强策略
Firefox 目前需要 flag,Safari 支持在 18.2+,对兼容性有要求可以渐进增强:
.tooltip {
/* 不支持时的兜底 */
position: absolute;
top: 100%;
left: 0;
}
@supports (anchor-name: --x) {
.tooltip {
position-anchor: --my-anchor;
position-area: bottom span-all;
position-try-fallbacks: --flip-top;
}
}
@supports (anchor-name: --x) 能检测锚点定位支持,不支持时用传统 CSS 兜底,不需要特征检测库。
迁移账怎么算
Mintec 在 Q2 2026 年做了三个客户项目的迁移,实测数据:INP 分数提升(弹层定位从主线程 JS 计算变成浏览器合成线程处理)、bundle 减少(删掉 Popper.js 3 KB 或 Floating UI 12 KB)、代码行数减少约 70%(去掉 createPopper 调用、ResizeObserver 监听、scroll 处理逻辑)。
如果你的项目还在用 JS 定位库处理 Tooltip/Dropdown/Popover,2026 年第三季度是值得迁移的时间窗口。锚点定位 API 规范已稳定,浏览器覆盖率达 88%,生产可用,渐进增强策略能覆盖剩余浏览器。
下一步
找项目里一个弹层组件,把定位逻辑抽出来,对照 MDN 的 @position-try 文档改成纯 CSS。先在本地跑一遍,确保滚动、resize、多个弹层同时出现的场景都正常——然后删掉那行 import Popper from 'popper.js',看看 bundle 少了多少。
评论区
登录后可评论。