以前做那些“点击按钮弹出气泡提示”的交互,最烦的不是交互逻辑,而是算位置。明明一个简单的小箭头,要么用伪元素乱怼,要么就上 JS 监听滚动、按键计算 left top,调半天还不一定精确。碰上窗口缩放直接就飞了,那体验叫一个酸爽。
最近 Chrome 把 CSS Anchor Positioning 正式推到了 125 版本,这玩意简直是浮层「天选之子」。你只要把两个元素绑一下,告诉浏览器“气泡跟谁绑”,然后指定一个方位,剩下的坐标计算全交给浏览器,甚至能根据可视区域自动翻转。
今天就拿一个非常常见的“点击按钮弹出带箭头气泡卡”来做例子,手把手用原生 CSS 把它实现出来。代码不多,但知识点绝对管饱。
1. 你要会的基础概念
CSS Anchor Positioning 的核心就是把一个元素(我们叫浮层)绑定到另一个元素(我们叫锚点)上。当锚点移动或者滚动时,浮层会自动跟随。它有两个关键属性:
anchor-name:给锚点元素起个名字,就像--anchor-name这种。position-anchor:浮层使用这个属性指向锚点的名字,建立绑定关系。
更妙的是,你用 position-area 来决定浮层出现在锚点的哪个方位。以前得靠 top/left 计算,现在直接写 --top=10px 之类的玩意儿。官方还提供了 anchor() 函数,可以手动计算绝对位置,但大部分场景咱们用 position-area 就够了。
2. 先看 HTML 结构
做一个按钮和气泡。我故意把气泡放在按钮外边,这样结构清晰:
<div class="wrapper">
<button class="trigger">点我弹气泡</button>
<div class="bubble" role="tooltip">
<div class="bubble__arrow"></div>
<p>这是锚点定位产生的气泡</p>
</div>
</div>
注意,气泡不需要放在按钮里面,这一点很自由。
3. 给按钮设置锚点名
在按钮的样式里,加一个 anchor-name:
.trigger {
anchor-name: --btn;
}
这个名字随便起,只要以 -- 开头就行。
4. 把气泡和按钮绑起来
在气泡上通过 position-anchor 指定目标锚点:
.bubble {
position-anchor: --btn;
position: fixed;
position-area: top center; /* 出现在按钮上方,水平居中 */
}
这里关键的 position-area 语法很直观:第一个值表示垂直位置(top, bottom, center, span-top 等),第二个值表示水平位置(left, center, right 等)。我正在用的 top center 意思是“放在锚点顶部,水平居中”。
但你现在直接这样写,会发现气泡还是出现在左下角错位。别急,因为还得给 position-area 设置一个可感知的区域范围。你可以把 position-area 想象成 CSS Grid 里的 place-content。为了让它生效,你最好给浮层加一个 position-fallback 或者直接定义 position-try-fallbacks,但初始最好用 position-area 配合 calc-size 之类的。
实际上,直接使用 position-area 必须搭配 position-try-fallbacks 来兼容不支持的情况。不过我们这里先不整那么复杂的。让我给气泡加一点绝对尺寸和颜色,方便你看效果。
.bubble {
position-anchor: --btn;
position: fixed;
position-area: top center;
width: 200px;
background: #1f2937;
color: #fff;
padding: 12px;
border-radius: 8px;
}
这时你运行代码,气泡会正确出现在按钮上方,水平居中。是不是很神奇?不用算 offsetLeft 和 offsetTop,浏览器自动搞定。
5. 让小箭头听话地对齐
气泡上的小箭头往往需要指向按钮中心。以前得用负 margin 或者 transform 微调,现在我们可以用 anchor() 函数直接拿到锚点的坐标,再给箭头定位。
给箭头添加绝对定位属性:
.bubble {
position: relative; /* 为了让箭头绝对定位 */
}
.bubble__arrow {
position: absolute;
width: 12px;
height: 12px;
background: #1f2937;
transform: rotate(45deg);
left: anchor(--btn center); /* 关键:水平位置使用锚点的中心 */
top: 100%; /* 放在气泡底部 */
margin-left: -6px; /* 让箭头的中心点对齐 */
margin-top: -6px;
}
这里我用了 anchor() 函数,它的语法是 anchor(锚点名 属性),例如 anchor(--btn center) 会返回锚点中心到某个参考点的距离。配合绝对定位,就把箭头精准地放在按钮的正上方了。
注意,请确保 left: anchor(--btn center) 中的 center 是包含在 anchor() 里的属性,这是一种关键字引用。Chrome 125 已经支持。
6. 让气泡自动翻转避免溢出屏幕
这是锚点定位最爽的功能之一。默认情况下如果按钮靠近视口顶部,气泡往下弹出可能更合适。以往你得用 JS 判断,现在原生 CSS 就能自动解决。
使用 position-try-fallbacks 定义一组可替换的位置策略。比如:
.bubble {
position-try-fallbacks: flip-start, flip-end, flip-block;
}
这只是一些术语,更直接的做法是使用 position-try-fallbacks 配合 position-area 的变体。比如:
.bubble {
position-area: top center;
position-try-fallbacks: bottom center;
}
这样如果上方空间不足,浏览器会自动尝试把气泡放到按钮下方。多写几个 fallback 就会智能选择第一个能够在视口内完整显示的位置。
但为了简单,我只给一个备用方案:
.bubble {
position-area: top center;
position-try-fallbacks: bottom center;
}
看,就这么单纯。浏览器会优先试试 top center,如果放不下就换成 bottom center。这逻辑要是自己用 JS 写,怕是要崩溃。
7. 完整示例代码
把所有东西合在一起,加一点过渡动画效果,就是一个完整可用的组件了。CSS 代码写在 style 里(其实真实项目里你用 <style> 没问题,这里为了展示而已):
<!DOCTYPE html>
<html>
<body>
<div class="wrapper">
<button class="trigger">点我弹气泡</button>
<div class="bubble" role="tooltip">
<div class="bubble__arrow"></div>
<p>这是锚点定位产生的气泡</p>
</div>
</div>
<style>
.trigger {
anchor-name: --btn;
}
.bubble {
position-anchor: --btn;
position: fixed;
position-area: top center;
width: 220px;
background: #0f172a;
color: white;
border: 1px solid #2d3a4f;
border-radius: 10px;
padding: 14px;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
opacity: 0;
transform: scale(0.9);
transition: opacity 0.2s, transform 0.2s;
/* 自动翻转策略 */
position-try-fallbacks: bottom center, top right;
}
.bubble:popover-open,
.bubble[open] {
opacity: 1;
transform: scale(1);
}
.bubble__arrow {
position: absolute;
width: 12px;
height: 12px;
background: inherit;
border-left: inherit;
border-top: inherit;
transform: rotate(45deg);
left: anchor(--btn center);
top: 100%;
margin-left: -6px;
margin-top: -6px;
background: #0f172a;
border-left: 1px solid #2d3a4f;
border-top: 1px solid #2d3a4f;
border-radius: 2px;
}
</style>
</body>
</html>
这段代码里我用了 popover-open 伪类,那是配合 Popover API 的。如果你想让点击按钮切换显示,你可以在按钮上加 popovertarget 指向气泡的 id。但 Popover API 是另一个话题了。锚点定位和 Popover 并不冲突,反而天生一对。这里就不展开,只关注锚点定位本身。
8. 兼容性以及降级方案
锚点定位目前只有 Chrome/Edge 125+ 支持,Firefox 正在开发中,Safari 还在考虑。所以如果你要做线上产品,最好加一个降级方案。
最简单的降级就是只把锚点定位作为“增强手段”。比如你可以写一个备用样式,把气泡放在按钮正下方,用普通 transform 定位;当浏览器支持 anchor-name 时,再用锚点定位覆盖。可以用 @supports 判断:
@supports (anchor-name: --btn) {
.bubble {
position: fixed;
position-area: top center;
}
}
如果不支持,就老老实实用 position: absolute + JS 计算吧,不过那种情况会越来越少。
9. 还有什么好玩的?
锚点定位不只能做气泡。你可以把两个元素任意关联,比如让一个注释框指向文档中的某段文字,或者做一个跟随鼠标的标尺。甚至可以把一张图片的某个局部和解释标签绑定。有了 anchor() 函数,你可以拿到锚点的 top, left, right, bottom, center 等各种值,想怎么组合都行。
我尤其喜欢的是它能解决“动态尺寸”问题。以前浮层大小变化后,因为坐标是 JS 算死的,可能又要重新算一遍。现在浏览器每帧自动测量,缩放窗口也无忧。
10. 个人感受
CSS 这两年真的在狂补“应用交互层”的功课。以前需要大量 JS 才能实现的相对定位,现在慢慢变成一段声明式 CSS。虽然锚点定位目前还只能在部分浏览器里跑,但趋势已经很明显了:浏览器正在变成强大的布局引擎。
我以后做新项目,凡是遇到“某个元素指向另一个元素”的场景,第一时间就会想到锚点定位。再也不想回到手写 getBoundingClientRect 再算偏移的老路了。
这篇文章不是劝你把所有线上代码都换成锚点定位,只是希望你在适当的时候能有一个新选择。等 Safari 和 Firefox 追上来,这套东西必火。
最后,如果你也想试试,直接把上面的代码贴到 Chrome 125+ 的浏览器里运行。反正我是在一个内部后台系统里用上了,大大解放了双手。

