浏览器终于开窍了,HTML 现在能原生创建气泡、菜单、通知,关键是不用写一堆 JS 控制显示/隐藏,也不用非要套一个 dialog。今天拿 Popover API 做个通知中心和一个下拉菜单,保准你用过就不想搞老一套。
Popover 是什么?
简单说,popover 属性让一个元素藏起来,点击某个按钮就能切换显示。还能点外部自动关闭、按 Esc 关闭,那些历史上自己写的点击外部隐藏逻辑一下子全不要了。
最基础用法就是一个按钮加一个带 popover 的元素,然后用 popovertarget 关联上。
<button type="button" popovertarget="my-popover" popovertargetaction="toggle">点我看看</button>
<div id="my-popover" popover>
这是原生气泡
</div>
注意:popovertargetaction 还支持 show 和 hide,一般 toggle 就够用了。
先用一个通知例子试试水
通知一般停几秒就自己没了,其实 Popover 不管自动隐藏,但咱们可以自己加个 setTimeout 控制。
先放 HTML,模拟一个通知气泡:
<div id="notice" popover role="status">已保存修改</div>
<button type="button" popovertarget="notice" popovertargetaction="show">保存</button>
注意到 role="status" 吗?给读屏软件用的,屏幕阅读器会播报。原生弹窗终于能自然配合无障碍了。
自动关闭得用 JS 调 hidePopover()。其实也很简单,咱们加个小计时器:
const saveBtn = document.querySelector('[popovertarget="notice"]');
const notice = document.getElementById('notice');
saveBtn.addEventListener('click', () => {
notice.showPopover();
setTimeout(() => {
if (notice.matches(':popover-open')) {
notice.hidePopover();
}
}, 2000);
});
看起来还是有点小 JS。那有没有更“原生”的自动关闭?有,popover 配 interest 或者 light-dismiss 都不自动关闭。所以老实写个 setTimeout 就行。
真·下拉菜单:不用 JS 控制开关
做导航菜单,最烦的就是点击别处能不能收起。以前要监听全局 click,还要判断是否在元素内。现在直接在按钮上用 popovertarget 就完事了。
先来个实际的菜单布局:
<div class="user-menu-wrap">
<button class="avatar-btn" popovertarget="user-menu" popovertargetaction="toggle">
账号
</button>
<div id="user-menu" popover>
<div class="menu-item">个人资料</div>
<div class="menu-item">订单记录</div>
<div class="menu-item">退出登录</div>
</div>
</div>
啥 JS 都不用写,点按钮就切换,点菜单外部自动关,按 Esc 也能关。你要是愿意,还能给每个菜单项加上 data-action,再统一挂事件。
然后这里有个坑:如果菜单内部再有弹层(比如子菜单),得给子菜单加 popover="manual" 防止父菜单被点外面自动关掉。对,popover 有两个值:
popover="auto"(默认):点外部 / Esc 自动关,且同时只会显示一个 auto 弹层。popover="manual":手动开关,点外部不关,Esc 也不关。
导航菜单一般用 auto 就合适。但如果你做一个悬浮提示,想让它停留久一点,反而用 manual 更稳。
高级组合:Popover 和 CSS 锚点定位
今天的热门话题还有一个:锚点定位(Anchor Positioning)。简单来说,让气泡出现的位置尽量贴紧触发按钮,而不是默认居中或固定位置。这个 API 还在实验阶段,但 Chrome 已经能跑。
它和 Popover 配合,菜单就能做到绝对定位到按钮旁边,即使页面滚动也不会跑偏,而且无需 JS 计算位置。
举个例子,给按钮添加 anchor-name,然后让气泡用 position-anchor 绑定:
<style>
/* 注意:这属于实验性 CSS,不在本文样式范畴,仅演示结构 */
</style>
咱们不写样式,只说结构。但你不要真的完全不用 CSS,锚点定位必须要 CSS 设 anchor-name。其实 HTML 那边也有个 anchor 属性,但还没普及。直接看个代码意思:
<button class="anchor" popovertarget="tooltip">悬停我</button>
<div id="tooltip" popover="manual">我是提示</div>
CS 里大概这么配:
.anchor { anchor-name: --btn; }
#tooltip { position: anchor(--btn); top: anchor(--btn bottom); }
不过这个 CSS 牵扯到新知识,不建议今天就用,但可以当成以后的热点。
当心:Safari 和 Firefox 的兼容性
Chrome 117 早支持了,Edge 也都行,问题最大的是 Safari 直到 17.4 才支持,Firefox 目前仍然默认不支持 Popover API(截至 2025 年中,但 Flag 已经能开)。所以如果你要发布到面向国内用户为主的站点,那基本能用了,毕竟大家在 Chromium 核心上。
如果实在要兼容 Firefox,可以用 dialog 元素做降级的弹窗,或者用老掉牙的 div+class 切换,但那个咱们不写。
完整的可运行案例:消息通知中心
直接放一个能跑的例子。不用 CSS,只看到结构。
<!DOCTYPE html>
<html lang="zh">
<body>
<h3>消息通知</h3>
<button type="button" popovertarget="notification-pop" popovertargetaction="toggle">
查看未读消息 (3)
</button>
<div id="notification-pop" popover>
<h4>你有三条新消息</h4>
<ul>
<li>有人回复了你的评论</li>
<li>你的账单已生成</li>
<li>新版本上线,点击更新</li>
</ul>
<button popovertarget="notification-pop" popovertargetaction="hide">确实,已读</button>
</div>
</body>
</html>
等等,按钮“确实,已读”还是用 Popover 自己隐藏。你可以在这里做点 JS,比如点击后通过 fetch 把消息标记为已读,但这里就不画蛇添足了。
跑起来你会看到:点“查看未读消息”弹出来,再点一下按钮弹层关闭,按 Esc 也关闭,点击外面一层也会自动关。这些行为统统是浏览器默认的,没写一行 JS。
一个容易踩的坑
如果把 popover 元素放在隐藏的容器里,比如 display:none 的父级,它依然能通过 showPopover() 显示吗?答案是:可以,但是位置会有问题,因为气泡默认默认定位在视口中间,不会受父级影响。如果你想让气泡出现在按钮附近,依然需要 CSS 定位。所以通常把气泡直接放到 body 下面。
另外,popovertarget 指向的 id 必须是唯一的,如果页面上有两个相同 id,只有第一个会有效。这不用多说了吧。
还是得聊两句性能
原生 API 比用 JS 改 display 的性能高那么一丢丢,但主要优势是代码量少。尤其遇到需要多个可展开菜单的情况,不用维护一堆布尔变量。比如做一个 FAQ 列表,每个答案都是带 popover="manual" 的元素,点击问题切换。JS 就只需要几行 togglePopover(),比老方案清爽。
总之,Popover API 是 HTML 里少有的“功能补完型”特性,值得在项目里用起来。别怕它不是“新 CSS 魔法”,这东西才是真正的生产力。
总结一下
这次用 Popover 做了通知和菜单,核心点就三个:
popover属性让元素变成弹层;popovertarget关联按钮和弹层;popovertargetaction指定动作是开、关还是切换。
再记一个 popover="manual",用来控制是否允许外部点击关闭。后面等锚点定位全面支持,就能做到真正“贴着按钮”的菜单,到时候再写一篇。
浏览器厂商最近都在卷这种“原生组件”,咱们做前端的终于能少写一些封装的 dropdowm 函数了。不过玩笑归玩笑,你自己的项目还是得先重点看浏览器兼容,最好降级方案也留着。
完。

