配了三年浮层,每次想让下拉菜单和按钮一样宽都要靠 JS 读 offsetWidth——今天 CSS 自己会认了
写浮层组件最烦的不是定位本身,而是「宽度跟着触发器走」这个需求。select 下拉框要跟按钮一样宽,dropdown 面板要贴着触发器左边缘对齐,下拉框里选项多了还不能比触发器窄——这套逻辑以前要写 getBoundingClientRect() 加 ResizeObserver,每个浮层组件都在重复。CSS 锚点定位把这件事彻底原生化了。
Chrome 125+ / Edge 125+ / Firefox 147+ / Safari 26+,~83% 全球覆盖率,Baseline 2026,生产可用。
position-area:网格化定位比 anchor() 函数更简洁
锚点定位有两种写法。一种是 anchor() 函数,精确引用锚点某条边:
.tooltip {
position: fixed;
position-anchor: --btn;
bottom: calc(anchor(top) - 8px);
left: anchor(center);
translate: -50% 0;
}
另一种是 position-area(2024 年前叫 inset-area),把锚点想象成 3×3 网格的中心,浮层放在哪个格子由两个关键字决定:
.dropdown {
position: absolute;
position-anchor: --menu-btn;
/* 下方居左,span-inline-end 让面板从锚点左边缘延伸到右侧 */
position-area: bottom span-inline-start;
}
position-area 的值由行 + 列组成。行可选 top / center / bottom(或逻辑等价的 block-start / center / block-end),列可选 left / center / right(或 inline-start / center / inline-end)。加上 span- 前缀可以让浮层跨越多格——bottom span-all 是「下方横跨整行」,right span-top 是「右侧贴顶对齐」。
对于大多数浮层场景,position-area 比 anchor() 函数更直观,代码更短。
anchor-size():下拉菜单宽度跟着触发器走
这是本文的核心。传统做法:
// JS:读按钮宽度,设置给下拉菜单
const btn = document.querySelector('.menu-btn');
const menu = document.querySelector('.dropdown-menu');
menu.style.minWidth = btn.offsetWidth + 'px';
// 响应窗口变化
const ro = new ResizeObserver(() => {
menu.style.minWidth = btn.offsetWidth + 'px';
});
ro.observe(btn);
CSS 锚点定位的解法:
.dropdown {
position: absolute;
position-anchor: --menu-btn;
position-area: bottom span-inline-start;
/* 宽度等于锚点宽度,内容超出自动撑开 */
min-inline-size: anchor-size(width);
}
anchor-size(width) 实时读取锚点元素的宽度,浮层宽度始终等于按钮宽度。按钮在响应式布局里变宽,下拉菜单自动跟着变宽,零 JS。也可以用 anchor-size(height) 做高度匹配,calc(anchor-size(height) * 5) 做最大高度限制。
注意:要把 anchor-name 放在触发按钮上,不要放在按钮里的图标上,否则 anchor-size(width) 会拿到图标的宽度而不是按钮的宽度。
完整下拉菜单:三件套
/* 触发按钮 */
.menu-btn {
anchor-name: --menu-trigger;
}
/* 下拉面板 */
.dropdown-panel {
position: absolute;
position-anchor: --menu-trigger;
position-area: bottom span-inline-start;
margin-top: 4px;
min-inline-size: anchor-size(width); /* 匹配触发器宽度 */
/* 放不下时自动翻到上方 */
position-try-fallbacks: top span-inline-start;
}
@position-try --flip-above {
position-area: top span-inline-start;
margin-top: 0;
margin-bottom: 4px;
}
配合 popover 属性,连显示/隐藏都不用 JS:
<button class="menu-btn" popovertarget="my-dropdown">打开菜单</button>
<div id="my-dropdown" popover>
<ul>
<li><a href="#">设置</a></li>
<li><a href="#">通知</a></li>
<li><a href="#">退出登录</a></li>
</ul>
</div>
点击按钮 → popover 打开 → 浏览器自动定位 → 放不下自动翻转 → 点击外部自动关闭。全程无 JS 坐标计算,无 ResizeObserver,无事件监听。
三个坑
1. 属性名改了:2024 年规范把 inset-area 改名为 position-area,旧文章和部分工具生成的代码里还用 inset-area,新浏览器里会静默失效。如果 position-area 不生效,先检查是不是用了旧名字。
2. anchor-size() 的浏览器支持门槛比 position-area 高一点:Firefox 131+ 才支持,Safari 18.2+ 才支持。需要写兜底宽度:
.dropdown-panel {
min-inline-size: 180px; /* 兜底 */
}
@supports (anchor-size: width) {
.dropdown-panel {
min-inline-size: anchor-size(width); /* 升级版 */
}
}
3. 触发器宽度变化时动画问题:如果触发按钮在 hover/focus 时改变宽度(如 hover 时加边框或 padding 变化),浮层会跟着 resize。保持触发器在交互前后 box model 稳定可以避免这个问题。
三步下一步
- 跑一个 dropdown Demo:把
anchor-name+position-area: bottom span-inline-start+min-inline-size: anchor-size(width)+position-try-fallbacks: flip-block四件套跑起来,看下拉菜单宽度是否真的跟着按钮走 - 改造一个现有浮层组件:把
getBoundingClientRect()+ResizeObserver的宽度同步逻辑替换为anchor-size(width),删掉 30 行 JS - 加
@supports渐进增强:先声明min-inline-size: 180px等兜底值,再用@supports (anchor-size: width)包裹anchor-size()声明,让老浏览器也能用
anchor-size() 把「宽度跟着谁走」变成了 CSS 属性声明,设计师改设计稿不用再找前端改 JS——前端也不用再在组件里埋 ResizeObserver 了。
评论区
登录后可评论。