配了三年弹层组件,今天才发现根子不在定位逻辑——是浏览器终于把这件事还给了 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 leftbottom 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 少了多少。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 69 阅读