你说巧不巧,上周产品让我在页面上加一个“用户指南”的弹层,要求不用UI框架,点击按钮就弹出一段说明,点击空白处就关闭。按照以前的老思路,我第一反应是复制一段自己收藏的弹窗JS代码,改改样式,绑定click事件,再写一个点击外部关闭的if判断。但这次我留了个心眼,去MDN上翻了一圈,发现“元素弹窗”早就有原生API了,叫Popover。试了一下,好家伙,我才写了两行HTML,连一行JS都没写,弹窗就能用了。
Popover是什么
简单说,这是一个纯HTML属性,让你任何一个元素能变成“弹层”。这个弹层默认是隐藏的,触发后显示在最上层,并且支持点击页面其他区域自动关闭。用起来跟details标签似的,全靠浏览器干活。
基础用法就是在元素上加上popover属性,然后通过另一个元素的popovertarget属性来指向它。看下面这个最简单的例子:
<button popovertarget="tips">显示提示</button>
<div id="tips" popover>这是一条提示</div>
没错,button不用写onclick,div也不用写display:none。点一下按钮,div就显示出来,再点一下页面别的地方,div就自己消失了。一个JS函数都没写。
你需要知道的几个属性
popover:普通属性,加上后元素成为弹层,默认隐藏。popovertarget:触发按钮上使用,值是要弹出的元素id。popovertargetaction:可以指定是show还是hide,或者toggle(默认切换)。
所以如果你想要一个独立的“关闭”按钮,可以这样:
<button popovertarget="tips" popovertargetaction="show">打开</button>
<button popovertarget="tips" popovertargetaction="hide">关闭</button>
<div id="tips" popover>快来试试</div>
连JavaScript的showPopover()和hidePopover()都不用调,浏览器自己搞定。
更爽的:点击外部自动关闭
以前做弹窗,最烦的就是点击遮罩或页面其他区域关闭。你得写个监听器,判断点击目标是否在弹窗内部。现在用Popover,浏览器默认支持“light dismiss”,就是点弹层外部区域时自动关闭。这一下就省了十来行监听代码。
如果你不想让它在点击外部时关闭,可以添加manual属性,强制手动关闭。例如:
<div id="pop" popover manual>需要手动关的弹窗</div>
做一个完整实例:消息通知图标
空说无用,直接看一个常见的交互:一个铃铛按钮,点击后弹出几条通知。我用Popover来做,比之前写Vue组件感觉还省事。
<button popovertarget="notify" style="font-size:24px">🔔</button>
<div id="notify" popover>
<p>你有3条未读消息</p>
<ul>
<li>系统维护通知</li>
<li>有人回复了你</li>
<li>你的工单已关闭</li>
</ul>
</div>
我在按钮上直接加了个emoji当铃铛,点一下铃铛弹出来,点屏幕其它地方消失。你甚至可以给popover元素加上max-width或border样式,让它在视觉上更像真正的弹窗。
有一点需要留意,popover元素会自动获得position: fixed,默认放在页面上方中间(具体位置因浏览器而异)。如果你不想用它默认的对齐方式,可以通过CSS去覆盖它的位置,比如设置margin: auto并配合inset: 0来居中。但由于文章里不打算写样式,这里就点到为止。
和dialog元素有什么区别
我第一次看到popover,脑子里第一反应是“这不就是dialog吗”?其实两者差距很大:
dialog需要调用showModal()才会显示并锁住背景,而且默认会带来焦点陷阱,适合模态框。popover更轻量,适合非模态的悬浮提示、气泡面板、快捷菜单等。它不会锁住页面,也不强制管理焦点。
换句话说,如果是一个必须让用户先处理弹窗再继续操作的场景,用dialog;如果只是临时补充信息的悬浮层,用popover更顺手。
还能配合JavaScript做更多事
虽然Popover在纯HTML下已经很能打,但你也完全可以用JS控制。每个popover元素上有showPopover()、hidePopover()、togglePopover()方法。另外还有beforetoggle和toggle事件,可以让你在弹层显示或隐藏时执行额外逻辑。
比如我想在通知弹窗显示时给铃铛加个红点,就可以监听toggle事件:
const notify = document.getElementById('notify');
const bell = document.getElementById('bellBtn');
notify.addEventListener('toggle', (e) => {
if (e.newState === 'open') {
bell.classList.add('has-dot');
} else {
bell.classList.remove('has-dot');
}
});
需要注意,toggle事件里的newState只有open和closed两种值,非常清晰。
嵌套Popover需要注意的事
如果你在两个popover元素之间做嵌套(比如一个popover里又有一个按钮打开另一个popover),那就要小心了。因为默认点击外部关闭,如果你点内层popover的触发按钮,外层也一样可能被判定为“点击外部”而关闭。为了避免这种情况,最好使用manual属性,或者用JavaScript控制嵌套的关闭时机。
我实际测试中,这个嵌套场景有点玄学,不同浏览器表现不太一样。所以我的建议是:做简单的popover时,不要嵌套太深。如果需要复杂的级联菜单,还是老老实实用下拉组件库。
一个很大的坑:旧浏览器没法用
网上查了一下,Popover API大概在2023年底开始被Chrome和Firefox支持,Safari也在最近几个版本加入。如果你要支持iOS 15、16那种老设备,那还是别用了。我开发的项目面向内部员工,大家浏览器版本都很新,所以直接用。如果是做面向大众的H5页面,你最好在Can I Use上查一下占比。不过这个API也支持通过polyfill来兼容,但那样就失去了“不写JS”的意义。
总结
Popover API让我最开心的一点是,“弹窗”这种极其常用的交互终于不用再依赖JavaScript了。你可以用几个纯按钮和div拼出一个功能完整的悬浮菜单。尤其适合那种不复杂的提示框:不用动画、不用遮罩、不用焦点管理。省下来的时间多写两行业务逻辑不好吗?
以后再做“出现一个气泡”这种需求,我肯定第一个想起popover,而不是翻开以前的代码库去复制那段已有一百八十行的弹窗插件。

