每次写浮动层都要装一个 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: hidden或transform会让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: absolute 或 fixed
position-anchor 只对绝对/固定定位元素生效。没有 position,anchor() 函数返回零。
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 已经原生支持了。
评论区
登录后可评论。