我电脑里现在还躺着一个年久失修的项目,里面有一个“悬浮提示”组件,是用几个div组成,通过加一个class控制显隐。点击关闭按钮的时候,要先把class去掉,再写一个setTimeout为了等动画结束再设置display:none。最恶心的是做个浅色的遮罩,还得另外用一大块样式。
前几天我偶然翻MDN,看到popover属性,突发奇想把它拿来重构那个提示层。改动少得我自己都惊讶:不需要管display,不需要记录状态。只管一个布尔属性,浏览器自己就把最苦的活全干了。
今天这篇文章,我不打算写包装N层的高深原理,就实打实从零把popover用明白,该讲的坑也带你踩一遍。
先弄明白popover到底是个什么
popover是HTML的一个全局属性。任何元素加上popover后,它就进入“隐藏”状态,只在调用打开方法时才出现。关键点是,它会在当前页面的最上层渲染,不受overflow和z-index干扰;不需要你把它渲染到body外面。
可以简单把popover理解为“浏览器原生支持的浮层”。跟dialog不同,它不会把背景变成不可交互。如果做一个模态框,还是得用它配套的inert去把后面的区域禁掉。但如果做常规的下拉菜单、气泡卡片、轻量通知,那popover就是天生的正主。
第一个例子:一个可以开关的popover
最简单的开启方式是配合一个按钮的popovertarget属性。如下面的代码,button 里的 target 指向一个popover的id。
<button popovertarget="notice">显示通知</button> <div id="notice" popover> <p>这是浏览器原生弹出来的内容。</p> </div>
打开这段HTML,什么都不用写JavaScript,点击按钮,那个div就会被浏览器以浮层的形式显示出来。再按一次Esc或者点击浮层外的区域,它又会自动关闭。
对,没听错,点击外部区域自动关闭是默认行为。这以前咱们要做几行代码监听全局click才能实现。
不只是开关:popovertargetaction 能指定动作
popovertarget只是一个目标连接。那另外还有一个属性叫popovertargetaction,它有三个值:toggle(默认)、show、hide。
show:强制打开目标popoverhide:强制关闭目标popovertoggle:如果当前隐藏就打开,如果显示就关闭
例如咱们可以这样做一个“打开”和“关闭”按钮,分开控制同一个浮层:
<button popovertarget="tip" popovertargetaction="show">打开提示</button> <button popovertarget="tip" popovertargetaction="hide">关闭提示</button> <div id="tip" popover> 这是提示的内容 </div>
这样是不是比你自己拿一个全局变量存状态要省心多了?
两大模式:auto 和 manual
popover可以带两种不同的关闭策略:
popover="auto":默认值,也叫轻量关闭模式。特点是点击popover外区域、按Esc、或者在同一个同组内打开另一个popover,它都会自动关闭。popover="manual":手动模式。只能通过调用JS或点击带有popovertargetaction=”hide”的按钮来关闭。点击外部和Esc都不会关闭。
比如做一个操作菜单,肯定需要点击外部自动隐藏,所以用auto。但如果你要做的是一个“设置向导”或者“图片预览小窗”,你可能希望用户不能点一下就把它关了,那就更适合manual。
要留意的是,popover元素如果设了popover="manual",它就不会形成“顶层层叠上下文”里的同组互斥效果。auto类型的popover相互之间有互斥。简单说,如果你在同一时间打开两个auto popover,后打开的那个会自动把先前那个关闭。
案例:做一个不带任何JS的收藏图标浮层
咱们拿一个场景来实操:页面上有一批文章卡片,每一张上面有一个“小眼睛”图标,点击后弹出一个层,里面放着“赞、收藏、分享”三个按钮。
按老办法,你得给每个图标写一个事件,然后用一个绝对定位div显示在对应按钮附近。用popover写其实思路不同:把popover元素和卡片放一起,点击卡片里的哪个按钮,按钮就指向对应的popover对象。
先看片段(只列两篇文章,循环生成也行):
<article>
<h3>这是一篇文章标题</h3>
<button popovertarget="actions_1">操作</button>
<div id="actions_1" popover>
<button>👍 赞</button>
<button>⭐ 收藏</button>
<button>↗ 分享</button>
</div>
</article>
<article>
<h3>另一篇文章标题</h3>
<button popovertarget="actions_2">操作</button>
<div id="actions_2" popover>
<button>👍 赞</button>
<button>⭐ 收藏</button>
<button>↗ 分享</button>
</div>
</article>
效果如何?你只要点击哪张卡片的“操作”,对应那个popover就会出现;点击页面其他地方,浮层自动消失。两个浮层不会同时打开,因为popover默认auto,天生互斥。这逻辑没写一行JavaScript。
当然,位置还比较朴素,因为没加CSS去锚定它,整体看起来像个居中浮层。如果想要它挨着按钮出现,你可以用锚定定位或绝对定位给pop-over套一层包裹。但这不是popover本身该背的锅。
我还需要说明一点:popover内部内容不要添加tabindex等,因为聚焦会自动进入内部。当打开时如果里面没有可聚焦元素,则焦点会落到popover本身,方便读屏。
把浮层藏到按钮里面:popover + 表单
popover虽然是挂在HTML属性上,但它也能用JavaScript来控制。有时候一个按钮不仅要触发业务逻辑,还要顺便把浮层关掉,那就直接调用 hidePopover()。
看一个收藏按钮的实际写法:点击“已收藏”按钮后,先改文字,再把popover关掉:
<button id="favBtn" popovertarget="favPop" popovertargetaction="toggle">收藏这篇文章</button>
<div id="favPop" popover="manual">
<button id="confirmFav">确定收藏</button>
</div>
<script>
const confirmBtn = document.getElementById('confirmFav');
const favPop = document.getElementById('favPop');
const favBtn = document.getElementById('favBtn');
confirmBtn.addEventListener('click', () => {
favBtn.textContent = '已收藏';
favPop.hidePopover();
});
</script>
这里popover用了manual,所以如果不点“确定收藏”或者用代码调用hidePopover,它就不会自己消失。这种适合轻量确认场景。
监听 popover 的打开和关闭
如果你需要知道浮层到底开了没,可以在popover元素上监听beforetoggle事件。事件对象里有一个newState属性,值为"open"或"closed"。
const notice = document.getElementById('notice');
notice.addEventListener('beforetoggle', (event) => {
if (event.newState === 'open') {
console.log('弹层即将打开');
} else {
console.log('弹层即将关闭');
}
});
配合这个事件你可以做:当popover打开时向服务器发送统计数据;关闭时重置内部状态。这个事件在动画之前触发,所以很适合做一些初始化。
还有一个更晚的toggle事件,在状态彻底变更后再触发。如果你只想在浮层完全消失后清理资源,那用toggle就行。
坑位提醒:为什么popover没样式也要注意层级
popover有一个非常霸道的样式行为:它一旦打开,浏览器会自动把它放到最顶层(top layer)。就是说页面里其他元素,不管z-index多大,都不可能盖住它。
这跟你用position:fixed或absolute完全不同。因为二者所在的层级不一定能超过普通元素。Top layer可以理解为浏览器专门开的一层,类似于原生全屏视频。
这一点既是优点也有一点麻烦:你不能在popover上面再放一个自己写的遮罩,或者把popover盖在另一个popover上。不过实际业务中很少遇得到。
关于兼容性,可以放心用吗
到了2025年,现代浏览器Chrome、Edge、Firefox、Safari都已经支持popover和它的触发属性了。如果你在一些旧的WebView里使用,最稳妥的降级办法是提前判断:
if (HTMLElement.prototype.showPopover) {
// 原生popover可用
} else {
// 调用自定义的老弹层
}
不过这个特征检测也存在风险,某些老浏览器即使没有showPopover,你也无法阻止它显示出来(因为它不支持popover属性时,只是把它当作普通div,并且初始不隐藏)。这样就会使全部内容在静态页面中漏出。所以更保险起见,可以把popover里原有的内容设计成按钮触发的工具提示,而不是那些没有触发就必须不可见的重要信息。
最后:popover不是弹窗杀手,它是普通浮层的救星
很多人说“以后可以直接用popover替代所有弹窗了”,这是一种误解。真正需要模态框的时候,还是应该用dialog配合modal语义,阻断背景交互。popover更适合那些短暂、非阻断、轻量的提醒。
但你在做下拉菜单、气泡卡、收藏夹、列表筛选、工具提示这类UI时,只要用上popover,就能减少很多手动控制逻辑。
我现在已经打算把我那个老项目里所有用div手搓的浮层全部换成原生的popover。虽然免不了写一些CSS去控制位置和动画,但至少那些“点击外面是否关闭、按Esc能否退出、多个弹层会不会互顶”之类的零碎逻辑都交给浏览器处理了。省下的不是发呆时间,是晚上熬掉的那些头发。

