配了八年弹层定位,今天发现九宫格把整个体系彻底变了——position-area + @position-try 完整方案
写过弹层组件的,都有一个共同感受:定位本身不复杂,复杂的是它“跑出去”之后怎么办。屏幕边缘、滚动视口、容器边界——弹层被截掉的那一瞬间,之前的计算全白费。
这件事,CSS 以前真的管不了。所以才有了 Popper.js 和 Floating UI——靠 JavaScript 算坐标、监听滚动、用 ResizeObserver 重算,再用 flip 逻辑决定弹到哪一边。这套方案管用,但包袱也重:一个运行时库,几千行代码,每次渲染都要重新算。
2026 年,CSS Anchor Positioning 进了 Baseline,83%–91% 的浏览器都支持了。这件事彻底变了——浏览器自己的布局引擎,把弹层定位这整条链路原生接管了。
position-area:把弹层放进九宫格
弹层定位最朴素的需求是:相对于一个锚点,我的元素应该出现在它的上方、下方、左侧还是右侧。position-area 就是用来干这件事的。
它基于一个 3×3 的网格概念:锚点元素是中心的格子,周围 8 个格子分别对应不同的相对位置。
核心属性是两个:
anchor-name:在锚点元素上注册一个名字position-anchor:在弹层元素上声明它跟随哪个锚点position-area:声明弹层出现在锚点的哪个区域
/* 锚点元素 */
.trigger {
anchor-name: --my-trigger;
}
/* 弹层元素 */
.panel {
position: absolute;
position-anchor: --my-trigger;
position-area: top center; /* 出现在锚点的上方居中 */
margin-bottom: 8px;
}
position-area 的值是两个关键字的组合,第一个表示行(top/center/bottom),第二个表示列(left/center/right)。九宫格布局:
| top left | top center | top right |
|---|---|---|
| left | (锚点) | right |
| bottom left | bottom center | bottom right |
只写一个关键字也可以,效果是自动 span-all(占满整行或整列)。top 等于 top span-all,bottom 等于 bottom span-all。
position-area 支持物理值(top/left/bottom/right)和逻辑值(block-start/inline-end)两种写法。混写会失效,只能选一套。
浏览器自己会找路:position-try-fallbacks
九宫格定位只是第一步。真正的问题是:锚点在页面顶部时,上方的格子直接被视口截掉;锚点贴左边缘时,右侧格子撑出屏幕外。这个“被截掉”的问题,之前只能靠 JavaScript 的 flip 逻辑处理。
CSS 现在原生接管了,叫 position-try-fallbacks。
.panel {
position: absolute;
position-anchor: --my-trigger;
position-area: top center;
margin-bottom: 8px;
/* 首选上方;如果放不下,自动翻到下方 */
position-try-fallbacks: flip-block;
}
flip-block 的语义很清楚:沿着块轴翻转。top center 翻成 bottom center,top left 翻成 bottom right。
同样还有 flip-inline(沿内联轴翻转)、flip-start(沿对角线翻)。
.panel {
position-try-fallbacks:
flip-block, /* 先试对边 */
flip-inline, /* 再试侧边 */
flip-block flip-inline; /* 最后试对角 */
}
多个 fallback 按顺序尝试,第一个能放下的就停下。
flip-block flip-inline 这个组合是实际使用中最容易被忘记加的。如果锚点在左上角,单轴翻转只能解决一个方向,两个方向同时溢出时必须用组合值。
自定义后备规则:@position-try
内置翻转关键字覆盖大多数场景,但有时需要更精细的控制。比如带箭头的气泡框,翻转后箭头要反向——这个靠 flip-block 改不了。
用 @position-try 自定义后备规则:
@position-try --above-with-arrow {
position-area: top center;
margin-bottom: 8px;
}
@position-try --below-with-arrow {
position-area: bottom center;
margin-top: 8px;
}
.panel {
position: absolute;
position-anchor: --my-trigger;
position-area: top center;
margin-bottom: 8px;
position-try-fallbacks: --above-with-arrow, --below-with-arrow;
}
箭头方向通过 CSS 变量配合 @position-try 来同步切换,整个方案不需要一行 JavaScript。
@position-try 里可用的描述符包括 inset 属性、margin、size、self-alignment 以及 position-area。
position-try-order:优先选更宽的位置
有时候默认位置虽然能放下,但不是最优的——比如上方空间很小,下方反而更宽裕。
.panel {
position-try-fallbacks: --hold-left;
/* 优先选能放下最大宽度的方案 */
position-try-order: most-width;
}
position-try-order 有 normal、most-width、most-height、most-inline-size、most-block-size 几个取值,先按哪个维度最优来选后备方案。配合自定义 @position-try 使用时特别有用。
anchor() 函数:精确控制边缘
position-area 是声明式的九宫格,适合大多数场景。但如果需要更精细的非对称控制,用 anchor() 函数直接引用锚点边缘:
.tooltip {
position: absolute;
top: anchor(--btn bottom); /* 锚点的底边 */
left: anchor(--btn left); /* 锚点的左边 */
margin-top: 8px;
}
position-area 和 anchor() 可以互补使用,position-area 管方向,anchor() 管精确偏移。
2026 年能不能用:可以
Chrome/Edge 从 125 开始支持,Safari 从 26 开始(约 2026 年),Firefox 从 147 开始。全球覆盖率约 83%–91%,完全可以作为渐进增强方案上线。
之前名字有过调整:position-area 原来是 inset-area,position-try-fallbacks 原来是 position-try-options,现在都已更名,要注意旧文章里的写法在当前浏览器里不生效。
弹层组件可以直接卸载了吗
基本可以。Tooltip、Dropdown、Context Menu 这三类场景,anchor-name + position-anchor + position-area + position-try-fallbacks 全套下来,不需要任何 JS 计算。
更复杂的需求——比如气泡框的箭头跟随旋转——配合 @position-try 和 CSS 变量也能完整覆盖。目前仍需要 JavaScript 的主要是:计算动态宽度(比如弹层宽度要和锚点等宽)、跨窗口边界定位等边界情况。
下一步:如果你正在用 Popper.js 或 Floating UI,可以先找一个非核心的弹层组件,用 CSS Anchor Positioning 重写一版,用作渐进增强。新项目可以直接上,旧项目逐步迁移,不用等所有场景全覆盖再动手。
评论区
登录后可评论。