配了八年弹层定位,今天发现九宫格把整个体系彻底变了——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-allbottom 等于 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 centertop 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-ordernormalmost-widthmost-heightmost-inline-sizemost-block-size 几个取值,先按哪个维度最优来选后备方案。配合自定义 @position-try 使用时特别有用。

anchor() 函数:精确控制边缘

position-area 是声明式的九宫格,适合大多数场景。但如果需要更精细的非对称控制,用 anchor() 函数直接引用锚点边缘:

.tooltip {
  position: absolute;
  top: anchor(--btn bottom);   /* 锚点的底边 */
  left: anchor(--btn left);    /* 锚点的左边 */
  margin-top: 8px;
}

position-areaanchor() 可以互补使用,position-area 管方向,anchor() 管精确偏移。

2026 年能不能用:可以

Chrome/Edge 从 125 开始支持,Safari 从 26 开始(约 2026 年),Firefox 从 147 开始。全球覆盖率约 83%–91%,完全可以作为渐进增强方案上线。

之前名字有过调整:position-area 原来是 inset-areaposition-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 重写一版,用作渐进增强。新项目可以直接上,旧项目逐步迁移,不用等所有场景全覆盖再动手。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 24 阅读