每次写浮动层都要装一个 30KB 的库——今天 CSS 用三行把它原生化了

每次写浮动层都要装一个 30KB 的库——今天 CSS 用三行把它原生化了

配了五年浮动层,每次写个 tooltip 或下拉菜单,第一件事就是 npm install @floating-ui/react——然后花半小时对接 computePosition()、写 getBoundingClientRect()、接 ResizeObserver 监听边界。2026 年了,这件事 CSS 三行就能原生解决,不需要任何库。

CSS Anchor Positioning + position-try-fallbacks 这套组合拳,把浮动层定位和视口防溢出全部接管进了浏览器 layout 引擎。flip-block 一个关键字,替代了过去一整套 JS 防溢出逻辑。

痛点:当年为了写个 tooltip 到底牺牲了什么

传统浮动定位绕不开四个坎:

  • 跨 DOM 层级 — tooltip 不在按钮里,定位要靠 JS 硬算 getBoundingClientRect()
  • 视口边缘检测 — 按钮贴顶,tooltip 往上翻会被截,要写翻转逻辑
  • 滚动后重定位 — 页面滚动,tooltip 位置要跟着变,要监听 scroll 事件
  • 堆叠上下文打断 — 父元素 overflow: hiddentransform 会让 position: absolute 定位失效

这四条,每一条都是当年写原生定位时的噩梦。社区花了五年做出 Floating UI 和 Popper.js,就是来解决这些问题——代价是项目里多了一个 15KB 的运行时依赖,每次 re-render 都要跑一次 computePosition()

CSS Anchor Positioning 基础

Anchor Positioning 的思路很简单:给触发元素起个名字,浮动元素通过名字找它

/* 1. 给触发元素(锚点)命名 */
.trigger-btn {
  anchor-name: --my-tooltip;
}

/* 2. 浮动元素关联到锚点 */
.tooltip {
  position: absolute;
  position-anchor: --my-tooltip;

  /* 用 anchor() 函数取锚点坐标 */
  inset-block-end: anchor(top);       /* tooltip 顶部对齐按钮底部 */
  inset-inline-start: anchor(center); /* 水平居中 */
  translate: -50% 0;
  margin-block-end: 8px;
}

关键点:anchor-name 不依赖 DOM 层级。按钮和 tooltip 可以在页面任何位置,浏览器会自动解析关联——这直接打破了传统 position: relative 必须共享父容器的限制。

核心:position-try-fallbacks 替代整个翻转逻辑

这是替代 Floating UI 的关键一招。不用任何 JS,浏览器自动在元素超出视口时翻转方向:

.tooltip {
  position: absolute;
  position-anchor: --my-tooltip;
  inset-block-end: anchor(top);
  inset-inline-start: anchor(center);
  translate: -50% 0;
  margin-block-end: 8px;

  /* 三行替代整个翻转逻辑 */
  position-try-fallbacks: flip-block;
}

flip-block 是内置关键字,浏览器会在检测到上方空间不足时,把 tooltip 自动翻到锚点下方。同理 flip-inline 处理左右翻转,可以组合:

position-try-fallbacks: flip-block, flip-inline;

如果需要更精细的控制,可以用命名回退:

@position-try --flip-above {
  inset-block-end: auto;
  inset-block-start: anchor(bottom);
  margin-block-end: 0;
  margin-block-start: 8px;
}

@position-try --shift-right {
  inset-inline-start: anchor(left);
  translate: none;
}

@position-try --shift-left {
  inset-inline-start: auto;
  inset-inline-end: anchor(right);
  translate: none;
}

.tooltip {
  position-try-fallbacks: --flip-above, --shift-right, --shift-left;
}

浏览器按顺序尝试每个回退,第一个能完整显示的位置就被采用。没有 JS,没有 ResizeObserver,没有 requestAnimationFrame

浏览器支持与渐进增强

特性 Chrome/Edge Safari Firefox
anchor-name / position-anchor 125+ 18.2+ 支持中
position-try-fallbacks 关键字 125+ 26+ 支持中
@position-try 命名规则 128+ 26+ 支持中
position-try 简写 125+ 26+ 支持中

position-try 整个属性已在 Baseline 2026 正式稳定。对于还未支持的浏览器,渐进增强写法:

/* 有 CSS Anchor Positioning 的用 CSS,没有的降级到默认定位 */
@supports (anchor-name: --x) {
  .tooltip {
    position: absolute;
    position-anchor: --my-tooltip;
    inset-block-end: anchor(top);
    inset-inline-start: anchor(center);
    translate: -50% 0;
    margin-block-end: 8px;
    position-try-fallbacks: flip-block;
  }
}

性能:浏览器 layout 引擎 vs JS 手算

Floating UI 的 computePosition() 运行在 JS 的微任务队列里,每次调用都会触发一次 layout reflow。在低端手机上,60fps 滚动时这个差异是能感知到的。

CSS Anchor Positioning 完全不同——定位逻辑跑在浏览器的 layout 引擎里,每帧渲染循环自动重算,不需要 JS 参与,也不触发额外的 reflow。滚动流畅度直接对标原生 DOM 行为。

方案 JS 体积 定位计算时机 滚动响应
Floating UI ~15KB gzipped 微任务队列手动触发 依赖 ResizeObserver/scroll 监听
CSS Anchor Positioning 0KB 浏览器每帧自动 原生响应,无 JS 回调
Popper.js ~8KB gzipped 手动计算 手动监听

三个坑

1. 必须配合 position: absolutefixed

position-anchor 只对绝对/固定定位元素生效。没有 positionanchor() 函数返回零。

2. Safari 18.2-18.3 不支持 @position-try 命名规则

这两个版本支持 anchor()position-anchor,但 @position-try 命名回退要 Safari 26+ 才完整支持。生产环境优先用内置关键字 flip-block / flip-inline,命名规则作为增强。

3. 锚点和目标必须在同一文档

iframe 的锚点关联不 work。如果你的 tooltip 在 shadow DOM 里,确保锚点也在同一个 shadow root 内。

三步下一步

第一步,今晚就能试

在任意 tooltip 组件外层加 @supports (anchor-name: --x) 块,把 position-try-fallbacks: flip-block 加上,观察控制台是否有报错。如果你的用户主要是 Chrome/Safari 用户,覆盖率已经超过 85%。

第二步,评估能否卸库

如果项目里只有 tooltip、下拉菜单、select 面板这三种场景,测试两周后可以把 Floating UI 或 Popper.js 从依赖里删掉。用 npx depcheck 扫一遍,确认没有其他地方引用。

第三步,更新组件文档

把「防溢出翻转」从 JS 逻辑标注改成 CSS @position-try 方案。新人 onboarding 时看到三行 CSS 而不是 15KB 的运行时依赖,理解成本差一个数量级。


CSS 这几年接管的东西越来越多了。从颜色运算到条件查询,从动画触发到布局计算,现在连浮动定位也进了它的版图。下一个你想起来要装库的场景,也许 CSS 已经原生支持了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 16 阅读