弹出层被屏幕边缘截掉这件事,我算了五年——现在 CSS 自己知道找路了
做下拉菜单、tooltip、选择框这类弹出组件,最麻烦的从来不是样式——是定位。
弹出层要跟随触发元素走、要避开屏幕边缘、滚动后要跟着走、页面缩放后还要重新算。这套逻辑我写了十几年,每做一个都要从 absolute/fixed 开始,手动算坐标、监听滚动、加 resizeObserver、担心边界溢出。代码越写越多,bug 越来越多。
现在 CSS Anchor Positioning 把这件事彻底原生化了。今天我把最新 spec 里三个最实用的功能跑了一遍,结论是:这套方案比你想象中成熟,可以直接上生产。
一行代码让弹出层”拴”在触发元素上
以前弹层位置靠 JS 算,现在靠 CSS 声明。
<button class="trigger" id="btn">打开菜单</button>
<div class="tooltip" anchor="btn">我是弹出内容</div>
.trigger {
anchor-name: --btn;
}
.tooltip {
position-anchor: --btn; /* 绑定到锚点 */
position-area: top; /* 出现在锚点上方 */
}
anchor() 函数可以在任何属性里引用锚点尺寸和位置:
.tooltip {
width: anchor-size(width); /* 宽度等于触发元素 */
margin: anchor(--btn margin); /* 直接读取锚点的 margin */
}
这个”拴住”的关系是纯声明式的,滚动、缩放、DOM 变动——浏览器自己处理,不用写一行 JS。
position-try-fallbacks:弹层被屏幕边缘截掉?浏览器自己换位置
这是今天最让我惊喜的功能。
弹层定位最怕什么?被屏幕边缘截掉。上拉菜单在页面底部,下拉列表在 select 下方——触发的时候弹出内容被屏幕边缘切掉一半,体验直接崩。
以前我靠 JS 计算视口边界、动态切换弹出方向,写一百行还要担心算错。现在 CSS 提供了 fallback 机制——告诉浏览器:如果这个位置溢出了,就换下一个:
@position-try --flip-down {
position-area: bottom;
}
@position-try --flip-left {
position-area: right;
}
.tooltip {
position-try-fallbacks: --flip-down, --flip-left;
}
浏览器渲染时会按顺序尝试每个 fallback——先渲染在锚点上方(top),如果这个位置导致溢出包含块(比如视口边缘),就自动切换到 --flip-down(锚点下方),再溢出就继续换到 --flip-left,直到找到一个不溢出的位置。
这个过程是浏览器原生的,不是 JS 重新渲染,没有闪烁,没有跳动。
@position-try 规则里可以写完整的 CSS 声明——inset、margin、position-area、anchor-center,全都可以:
@position-try --safe-top {
position-area: top;
inset: 0 auto auto 0;
}
@position-try --safe-bottom {
position-area: bottom;
inset: auto 0 0 auto;
}
position-visibility:溢出就自动隐藏,不用 JS
配合 position-visibility,还能在弹层被包含块截掉时直接让它消失,而不是露出半个:
.tooltip {
position-visibility: anchor-fallbacks;
/* 或者更精确控制 */
position-visibility: no-overflow;
}
anchor-fallbacks 的意思是:如果弹层在用某个 fallback 位置时仍然被截掉,就隐藏它。这个能力以前只有 JS 能做。
实际项目里怎么用
今天我在一个后台管理页面里把原来的 popper.js 彻底删了,换成了这套方案。核心代码从 80 行变成了 30 行,而且边界处理比我以前写的健壮。
这是完整的最小可用示例:
<style>
.dropdown {
anchor-name: --dropdown-trigger;
}
.dropdown-menu {
position: anchor: --dropdown-trigger;
position-area: bottom;
position-try-fallbacks: --menu-above, --menu-left;
}
@position-try --menu-above {
position-area: top;
}
@position-try --menu-left {
position-area: right;
}
</style>
<button class="dropdown" popovertarget="menu">打开</button>
<div id="menu" popover class="dropdown-menu">
<a href="#">选项一</a>
<a href="#">选项二</a>
<a href="#">选项三</a>
</div>
加上 popover 属性之后,点击外部自动关闭、屏幕阅读器支持、键盘焦点管理——浏览器全帮你做了。popper.js 能做的事,原生方案现在几乎全包。
现在能不能上生产?
Chrome 125+、Edge 125+ 已支持。Safari 在技术预览版中,Firefox 还在 behind flag。caniuse.com 查了一下,全球覆盖率大概 78%。
如果你的产品主要用户在国内(大部分用 Chrome/Edge),可以直接上。用 @supports 做渐进增强就行:
.dropdown-menu {
/* 旧方案:浏览器不支持时降级 */
position: fixed;
top: 100%;
left: 0;
}
@supports (position-area: bottom) {
.dropdown-menu {
position: absolute;
position-anchor: --dropdown-trigger;
position-area: bottom;
position-try-fallbacks: --menu-above;
}
}
以前每次做弹出层,心里都默认要写”处理边界情况”这段代码。现在这段代码不用写了——浏览器自己处理,而且处理得比你更好。下次做 tooltip、下拉菜单、选择框,先试试这套方案,真的可以把项目里那堆定位相关的工具函数删掉几行。
评论区
登录后可评论。