CSS锚点定位+Popover实战:不写定位脚本做自动翻边的筛选面板

2026-10-08 0 416

后台列表页里那个「筛选」按钮,点开之后弹出的面板,以前是这么做的:打开时 getBoundingClientRect 读按钮坐标,给面板设 left/top,监听 scroll 和 resize 重算,屏幕右边放不下就手动改 left,下面是表格,滚动时还得同步更新。几十行代码,每加一个类似组件就复制一遍。

现在这套逻辑可以整段删掉。CSS 锚点定位(anchor positioning)加上 Popover API,位置关系用声明式写清楚,翻边交给浏览器自动挑,滚动跟随也是浏览器自己处理。下面用一个真实项目里的筛选面板,把值钱的细节都走一遍。

锚点定位的四个核心 API

概念很少,就四个东西。掌握了就能干活。

1. anchor-name:给锚点元素起个名字

.filter-btn {
  anchor-name: --filter-btn;
}

名字必须是 CSS 自定义标识符的形式,也就是两个短横线开头。这个名字是一个全局的「锚点引用」,可以被页面上任意一个定位元素使用——不需要两个元素是父子关系,甚至可以在不同的 DOM 分支里。

这里有个容易翻车的点:如果页面上有多个元素同时声明了 anchor-name: --filter-btn,浏览器不会报错,也不会取第一个,而是取文档顺序里最后一个。所以名字最好带上组件语义,别全站复用一个 --anchor。

2. position-anchor:告诉定位元素「你挂在谁身上」

#filter-panel {
  position: fixed;
  position-anchor: --filter-btn;
}

设了它之后,后面写 anchor() 就可以省略那个参数。省略不掉的时候也能写全,两种写法是等价的:

/* 等价 */
top: anchor(bottom);
top: anchor(--filter-btn bottom);

3. anchor():把位置算出来

anchor(bottom) 的意思是「锚点元素的下边缘所在的坐标」,求值出来就是一个长度。所以它天然可以参与 calc():

top: calc(anchor(bottom) + 8px);

这句话读起来就是「我的上边缘,等于按钮的下边缘再往下 8 个像素」。比 JS 里算 rect.bottom + 8 直白得多。

限制也得记住:anchor() 只能用在 inset 系列属性(top/right/bottom/left 以及 inset-*)和 width/height/min-width/max-width 这类尺寸属性里。写在 margin、transform: translate()、padding 里都不会生效,而且不报错,只是静默失效。第一次用的时候我就把 translate: anchor(bottom) 写了进去,调了半小时才发现。

4. @position-try:翻边方案

这是最省事的部分。把「右边放不下就靠右对齐」「下面放不下就翻到上面」写成命名的备选位置,浏览器会自己按顺序试,挑第一个能完整塞进视口的:

#filter-panel {
  position-try-fallbacks: --align-right, --above, --above-right;
}

@position-try --align-right {
  left: auto;
  right: anchor(right);
}

注意 @position-try 里的声明是叠加在基础样式上的,不需要把基础样式重写一遍,只写发生变化的那几条就行。

完整案例:订单列表的筛选面板

需求拆一下:

  • 面板贴在筛选按钮的左下角,往下留 8px;
  • 宽度至少跟按钮一样,但太窄了不好看,兜底 220px;
  • 右边超出视口就改成右对齐;
  • 下面空间不够就翻到按钮上方;
  • 表格滚动时面板跟着按钮走;
  • 按钮被滚出视口,面板自动收起来;
  • 点面板外面、按 Esc 都能关。

最后两条,Popover 自带。前面五条,CSS 搞定。整段代码如下。

HTML 结构

<div class="toolbar">
  <button class="filter-btn" popovertarget="filter-panel">筛选</button>

  <div id="filter-panel" popover>
    <label for="kw">关键字</label>
    <input id="kw" type="search" placeholder="订单号 / 手机号" autofocus>

    <fieldset>
      <legend>状态</legend>
      <label><input type="checkbox" value="paid"> 已付款</label>
      <label><input type="checkbox" value="shipped"> 已发货</label>
      <label><input type="checkbox" value="refund"> 退款中</label>
    </fieldset>

    <button type="button" id="apply">应用</button>
  </div>
</div>

面板就是 toolbar 里的一个普通 div,加了 popover 属性。没有 portal、没有手动挂到 body 上。popover 打开时会被提升到 top layer,所以哪怕外层容器写了 overflow: hidden,也不会被裁掉——省掉了一个很常见的 portal 需求。

CSS

.filter-btn {
  anchor-name: --filter-btn;
}

#filter-panel {
  /* popover 在 top layer 里,UA 样式给了 inset:0 和 margin:auto,
     这两行必须先清掉,否则后面的 left/top 会被居中逻辑搅乱 */
  inset: auto;
  margin: 0;

  position: fixed;
  position-anchor: --filter-btn;

  /* 默认位置:按钮左下角,往下挂 */
  left: anchor(left);
  top: calc(anchor(bottom) + 8px);

  /* 至少和按钮等宽,但不能窄于 220px */
  width: max(anchor-size(width), 220px);

  /* 锚点滚出视口,面板自动隐藏 */
  position-visibility: anchors-visible;

  /* 按顺序尝试备选位置 */
  position-try-fallbacks: --align-right, --above, --above-right;
}

@position-try --align-right {
  left: auto;
  right: anchor(right);
}

@position-try --above {
  top: auto;
  bottom: calc(anchor(top) + 8px);
}

@position-try --above-right {
  top: auto;
  bottom: calc(anchor(top) + 8px);
  left: auto;
  right: anchor(right);
}

anchor-size(width) 取的是按钮的实际宽度,外面套一个 max() 就完成了「不小于 220px」的约束。这里没有用 min-width,是因为 width 和 min-width 同时写的时候容易被后面的规则覆盖,而 max() 是一个确定的值,读起来也更清楚。

position-visibility: anchors-visible 是这轮新特性里最被低估的一条。默认值是 always,锚点滚出视口时面板会继续悬在半空或者贴在边缘。改成 anchors-visible 之后,锚点一旦不可见,面板自动隐藏,连 IntersectionObserver 都不用写。

JS 只留三行

const panel = document.getElementById('filter-panel');
const trigger = document.querySelector('.filter-btn');

document.getElementById('apply').addEventListener('click', () => {
  panel.hidePopover();
  trigger.focus();
});

打开、关闭、点外部关闭、Esc 关闭和焦点归还,全是 popover 的默认行为,不用写。唯一的业务逻辑是「点应用之后把焦点还给触发按钮」,一共两行。

五个我踩过的坑

坑一:忘了清 popover 的 UA 样式

现象是面板永远显示在屏幕正中间,怎么改 left/top 都不动。原因是 UA 样式表里给 popover 元素写了 inset: 0; margin: auto;,两者一配合就是标准的居中。在 #filter-panel 里加 inset: auto; margin: 0; 就正常了。记住这是用 popover + 锚点定位时必写的一行。

坑二:position-anchor 写在非定位元素上

position-anchor 只对建立了定位的元素生效。如果忘了写 position: fixed 或者 position: absolute,整段锚点相关声明全部静默失效,控制台一片安静。所以我把 position: fixed 放在这个规则块的第一行,方便一眼确认。

坑三:锚点元素被 display: none 隐藏

有些按钮在不同权限下会隐藏。如果锚点元素是 display: none,它不生成布局盒子,anchor() 就求不出值,面板会退回到 inset 的初始值附近,看起来像飘到了左上角。用 visibility: hidden 隐藏的话盒子还在,不会有这个问题,但按钮位置会占着。这类情况最好在业务层判断:按钮不可见时别让它打开面板。

坑四:@position-try 里写了重复的属性却忘了改 left

@position-try --align-right 里只写了 right: anchor(right),但基础样式里的 left: anchor(left) 还在。结果两个方向的 inset 都生效,元素被拉伸成一个横跨按钮到视口右边缘的超宽面板。备选方案里改哪一侧,就得把另一侧显式设成 auto。

坑五:以为 anchor() 能用在任何地方

前面提过,anchor() 的作用范围被限定在 inset 和尺寸属性。想通过 transform 做动画偏移,就得先算好 inset,让 transform 只负责形变。

调试和兼容

调试很省心。DevTools 里选中面板元素,Elements 面板上会有一个锚点图标,点一下就能看到一条虚线连到锚点元素上,翻边命中了哪个 @position-try 也能在 Styles 面板里看到被划掉的声明。

兼容性不要靠记浏览器版本号,直接特性检测:

@supports not (position-anchor: --a) {
  #filter-panel {
    /* 老浏览器的降级:让面板退回成绝对定位,
       这时它会被父容器的 overflow 裁剪,所以要保证 .toolbar 不裁剪 */
    position: absolute;
    inset: auto auto auto 0;
    margin-top: 8px;
    width: 220px;
  }
}

同时还要把 .toolbar 加上 position: relative,否则绝对定位的参照物是最近的非 static 祖先,位置会乱。

这个降级方案放弃了翻边和滚动跟随,但至少能用。如果项目对老浏览器的体验要求高,还可以再补一个十几行的 JS:打开面板时读一次 getBoundingClientRect() 设 left/top,再挂 scroll 和 resize 重算。写成渐进增强的路线——CSS 能跑就完全交给 CSS,跑不了才用 JS 兜底。

什么时候别用它

三种情况我会绕开锚点定位。第一种是需要兼容较老浏览器、又不能接受降级体验的 C 端项目,这时候老老实实上成熟的浮层库更省事。第二种是弹层内容复杂、需要焦点陷阱和滚动锁定的,用 <dialog> 加 showModal() 更合适,popover 本身不提供焦点陷阱。第三种是锚点元素在某个被 transform 创建的包含块里面,又要求定位元素跟着它做复杂变换的时候,两套坐标系的换算容易出问题,不如直接用 JS 同步。

除此之外,尤其是后台系统这类浏览器环境可控的场景,这套组合拳值得替换掉手里的浮层定位代码。省下来的那几十行 getBoundingClientRect,是实实在在的维护成本。

CSS锚点定位+Popover实战:不写定位脚本做自动翻边的筛选面板
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html CSS锚点定位+Popover实战:不写定位脚本做自动翻边的筛选面板 https://www.taomawang.com/web/html/2908.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务