配了三年浮层,每次想让下拉菜单和按钮一样宽都要靠 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-areaanchor() 函数更直观,代码更短。

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 稳定可以避免这个问题。

三步下一步

  1. 跑一个 dropdown Demo:把 anchor-name + position-area: bottom span-inline-start + min-inline-size: anchor-size(width) + position-try-fallbacks: flip-block 四件套跑起来,看下拉菜单宽度是否真的跟着按钮走
  2. 改造一个现有浮层组件:把 getBoundingClientRect() + ResizeObserver 的宽度同步逻辑替换为 anchor-size(width),删掉 30 行 JS
  3. @supports 渐进增强:先声明 min-inline-size: 180px 等兜底值,再用 @supports (anchor-size: width) 包裹 anchor-size() 声明,让老浏览器也能用

anchor-size() 把「宽度跟着谁走」变成了 CSS 属性声明,设计师改设计稿不用再找前端改 JS——前端也不用再在组件里埋 ResizeObserver 了。

评论区

0 条评论

登录后可评论。