配了五年浮层防溢出,今天才知道这件事从来不需要 JS——@position-try 把它彻底原生化了
写过浮层 UI 的人都踩过这个坑——下拉菜单弹出来跑到视口外面去了,以前只能靠 Popper.js/Floating UI 在 JS 里算 getBoundingClientRect() 然后 requestAnimationFrame 循环重算。今天这件事被 CSS @position-try 彻底原生化了。
核心问题:浮层定位为什么非靠 JS
浮层 UI(tooltip、下拉菜单、select 弹层)天然面临一个问题:它的位置取决于它自己的尺寸,而它的尺寸取决于它显示的内容——但内容是渲染完了才知道的。所以要么用 JS 读渲染后的尺寸再重算,要么在 CSS 里写死尺寸然后祈祷不溢出。
@position-try 解决的就是这个「等渲染完才知道往哪翻」的问题。它的工作方式很有意思:声明一批备用定位方案,浏览器在布局阶段自己决定用哪个——整个过程在浏览器布局引擎里完成,不需要任何 JS。
内置 flip 关键字:一行替代 Popper.js 的 flip middleware
最常见的场景是「往上跑不下就往下」。用内置关键字一行搞定:
.dropdown {
position: fixed;
position-anchor: --trigger;
position-area: top center;
margin-bottom: 8px;
/* 往上放不下?试试往下。往下也放不下?试试往左往右翻 */
position-try-fallbacks: flip-block, flip-inline;
}
flip-block 镜像 block 轴(top↔bottom),flip-inline 镜像 inline 轴(left↔right)。浏览器按顺序试,第一个不出界的就用了。没有任何 ResizeObserver,没有任何 scroll 监听,没有任何数学计算。
flip-start 是逻辑方向版,在 RTL 语言下自动镜像到正确方向,多语言产品直接受益。
自定义 @position-try:说清楚「什么情况换哪里」
内置关键字只能做镜像。如果你想「正常放下面,横向放不下就侧到右边去」,需要自定义块:
@position-try --flip-below {
position-area: bottom center;
margin-top: 8px;
margin-bottom: 0; /* 必须重置,否则继承主规则的 margin-bottom */
}
@position-try --to-right {
position-area: right center;
margin-inline-start: 8px;
margin-bottom: 0;
}
.dropdown {
position: fixed;
position-anchor: --trigger;
position-area: top center;
margin-bottom: 8px;
position-try-fallbacks: --flip-below, --to-right;
}
注意每个 @position-try 块里要独立写清楚所有尺寸和间距,不要依赖主规则的值继承——布局属性会跨块累加,不重置就会跑偏。
position-try-order:让空间最大的方案胜出
默认按你写的顺序选第一个不出界的。如果 UI 区域是不规则形状(侧边栏里的下拉),可以用 position-try-order 改成「哪个方向空间最大用哪个」:
.panel {
position-try-order: most-height; /* 优先选垂直空间最充裕的方案 */
position-try-fallbacks: flip-block, flip-inline;
}
逻辑关键字 most-block-size / most-inline-size 和物理关键字 most-height / most-width 都可以用。
配合 inset-area:常见位置用网格语法更简洁
position-area 用网格组合描述位置,比 anchor() 函数更直观:
.tooltip {
position: fixed;
position-anchor: --btn;
inset-area: top center; /* 等同于 top center 定位 */
}
可组合的方向值:top/bottom/left/right + center,可以加 span-left/span-right/span-top/span-bottom 做宽度延伸。精确间距还是要靠 anchor() + calc()。
配合 Popover API:零 JS 的完整浮层方案
@position-try 解决的是「放哪」的问题,popover 属性解决的是「怎么显示/隐藏」的问题。两者配合就是完整方案:
<button popovertarget="menu">菜单</button>
<ul popover id="menu">
<li><a href="#">首页</a></li>
<li><a href="#">设置</a></li>
</ul>
[popover] {
position: fixed;
position-anchor: --trigger;
position-area: bottom span-right;
width: anchor-size(width); /* 宽度自动匹配触发器,替代 sameWidth middleware */
position-try-fallbacks: flip-block, flip-inline;
position-visibility: anchors-visible; /* 触发器滚出视口时浮层也消失 */
}
弹出/关闭/点击外部关闭/Escape 关掉,全是浏览器原生行为,不需要任何 JS。anchor-size(width) 替代了 Popper.js 的 sameWidth: true,position-visibility: anchors-visible 替代了滚动容器内的显式隐藏逻辑。
兼容性:Baseline 2026,91%+ 全球覆盖
Chrome 125+、Safari 26+、Firefox 131+ 已全部稳定,2026 年 1 月达成 Baseline 2026 全球覆盖约 91%。popover 属性支持更早(Chrome 114/Safari 17/Firefox 125),独立使用也完全没问题。
position-try-order: most-height/most-width 目前 Firefox 还未支持,如果用在 Firefox 上会回退到顺序选择。position-visibility: anchors-visible Firefox 也在跟进。核心的 position-try-fallbacks + @position-try 四大引擎全部稳定,可以直接上线。
怎么迁移
新项目直接用。对于已有 Popper.js / Floating UI 的项目,可以逐个组件替换:
- 把
position改成fixed+position-anchor - 把
getBoundingClientRect+ 手动坐标改成position-area - 把 flip middleware 的 JS 逻辑改成
position-try-fallbacks: flip-block - 把
sameWidth改成width: anchor-size(width) - 把滚动容器内的显示/隐藏监听改成
position-visibility: anchors-visible
实测一个带 4 个 fallback 方案的下拉菜单,从 Popper.js 迁移后:CSS 6 行,JS 0 行,bundle 减少约 12KB,运行时重算完全消失(devcheolu.com 实测数据)。
下一步
如果你在用 React/Vue 组件库,先查文档里浮层组件是否已支持 anchor positioning——Headless UI、Ark UI 都在跟进。如果不支持,考虑给库提 PR,或者在业务代码里先用 CSS 原生方案,等库更新。
这个 API 背后真正有意思的是:它把「几何计算」这件事从 JS runtime 挪回了浏览器布局引擎——在 JS 里算坐标再写回 DOM,绕了一大圈,浏览器其实一开始就知道答案,只是没有接口让 CSS 声明「我需要知道什么时候会溢出」。现在有了。
评论区
登录后可评论。