说真的,我以前特别怕写弹窗和下拉菜单。只要产品经理提出“点击外面就关闭,按ESC也能关”这种需求,我心里就一颤。因为又要去翻dome的文档,又要监听各种事件,还要处理z-index覆盖的问题。直到我发现了原生HTML的popover属性——这玩意儿简直是浮层界的亲儿子,能省太多事。
这篇文章不是什么教科书式讲解,就是拿我这两天实际用到的例子,给你演示popover到底能做什么、怎么用、有哪些坑。
初体验:两个标签,弹层就出来了
让一个元素变成悬浮层,只需要在标签上加一个叫做popover的属性。那怎么显示它呢?用一个按钮加上popovertarget指向这个悬浮层的id就可以。看下面这组代码,几乎没有一行JavaScript。
<button popovertarget="tips">查看提示</button>
<div id="tips" popover>这是用一个属性调出来的弹层</div>
打开浏览器点击一下,这个div就会出现在页面最上层。再点一下其他地方,它就自动消失了。就这么简单。
而且它不是一个普通的定位元素,它是被放在浏览器“顶层”里的。这意味着它天然不会被别的高z-index元素盖住,也天然具备“轻触关闭”的行为。以前我给弹层设z-index:999999的日子,算是彻底到头了。
带遮罩的模态框?原来也能白捡
模态框和普通弹层的区别是:模态框通常有一个半透明的遮罩,并且禁止用户点击页面的其他部分。popover虽然没有直接给一个“显示遮罩”的属性,但它有一个对应的伪元素::backdrop,专门用来画遮罩。你只需给它配几行CSS就能拥有漂亮的模态框。不过我这里重点讲行为,样式可以先忽略。
实现一个像样的模态框,除了popovertarget之外,还可以通过popovertargetaction控制具体动作。比如下面这个例子,点击外层按钮打开,点击弹层里的按钮关闭。
<button popovertarget="modal" popovertargetaction="show">打开模态框</button>
<div id="modal" popover>
<h2>这是一个模态框</h2>
<p>点外面或者点下面的按钮都能关闭</p>
<button popovertarget="modal" popovertargetaction="hide">关闭</button>
</div>
你肯定会问:如果用户点击模态框外面,会自动关闭吗?默认的popover属性是auto,会自动关闭。所以在大多数场景下,比传统的模态框体验还要好。如果你不想点外面关闭,就把popover的值改成manual,这样就只受代码控制。
实战:做一个右键菜单
我最近做一个小工具,需要在网页里实现自定义右键菜单。放在以前我肯定用JavaScript创建一大堆DIV,先把旧menu删掉再添加新的,还要监听滚动、点击空白区域隐藏。现在用popover,JS部分只干一件事:设置菜单位置。显示、关闭、ESC键、点空白隐藏全是浏览器自己的行为。
看下面的HTML结构,右键菜单本身是一个popover,内部有三个按钮:
<div id="ctxmenu" popover="manual">
<button data-action="copy">复制</button>
<button data-action="paste">粘贴</button>
<button data-action="delete">删除</button>
</div>
<main>在这个区域点击鼠标右键试试</main>
JS部分只需要很小一段。捕获contextmenu事件,阻止原生菜单,把popover元素移动到合适坐标,然后调用showPopover()显示出来。
const menu = document.getElementById('ctxmenu');
document.querySelector('main').addEventListener('contextmenu', function(event) {
event.preventDefault();
// 给popover设置位置
menu.style.left = event.clientX + 'px';
menu.style.top = event.clientY + 'px';
menu.style.position = 'fixed';
// 显示菜单
menu.showPopover();
});
menu.addEventListener('click', function(event) {
if (event.target.dataset.action) {
console.log('执行操作:', event.target.dataset.action);
menu.hidePopover();
}
});
注意一点,我给popover设置了popover="manual"。因为右键菜单需要手动控制,不希望它因为点击其他地方就自动关闭。这个“手动”模式就像一个听话的抽屉,只有你主动调用hidePopover()才行。所以代码里最后点击菜单项时主动关闭了它。
最终效果是:菜单出现在鼠标右下角,点击菜单项执行操作,按ESC键也能关闭。这体验和系统原生菜单没什么区别,而且不用处理一大堆焦点、遮罩、堆叠问题。
不用再愁 z-index 和遮挡了
popover最爽的就是“顶层元素”机制。它会被浏览器提升到一个特殊的顶层(Top Layer),无论你页面里的DIV嵌套多深,无论哪个倒霉的组件设置了什么离谱的z-index,popover永远显示在最上面。这对团队协作来说太友好了——你再也不用为了一个下拉菜单和同事吵架“他的z-index为什么压了我的菜单”。
我甚至可以断言,以后做全局浮层、用户通知、图片预览,原生popover会是首选。因为它不再是CSS游戏,而是浏览器行为。
auto 和 manual 到底选哪个?
- auto(默认):适合提示气泡、下拉菜单、临时对话框。它会自动关闭,并且同一时间只能有一个auto popover显示。比如你打开第一个auto弹层后点击另一个按钮打开第二个,第一个会自动关掉,这个行为太自然了,很多UI框架都做不到这么省心。
- manual:适合模态框、右键菜单、自定义Select下拉。因为它不会自动关闭,你需要自己控制显示和隐藏。好处是可以同时打开多个manual弹层,互不干扰。
<div id="manual-popover" popover="manual">我只能被手动关闭</div>
<div id="auto-popover" popover>点外面我会自己消失</div>
再做个小案例:自定义Select控件
HTML原生的<select>样式很难改。之前我都是模拟一个div列表。现在用popover就简单很多。按钮显示当前选择项,popover容器里放几个“选项”。点击选项后设置按钮文字,然后调用togglePopover()把浮层关掉。
<button id="selectBtn" popovertarget="options">请选择</button>
<div id="options" popover="manual">
<div class="select-option" data-value="苹果">苹果</div>
<div class="select-option" data-value="香蕉">香蕉</div>
<div class="select-option" data-value="橘子">橘子</div>
</div>
const btn = document.getElementById('selectBtn');
const options = document.getElementById('options');
options.addEventListener('click', function(event) {
if (event.target.classList.contains('select-option')) {
btn.textContent = event.target.dataset.value;
options.togglePopover();
}
});
这个Select能获得焦点响应吗?如果要求无障碍支持,可以加上ARIA属性。但是popover把这个“伪Select”的行为框架搭好了一半。你再也不用自己去判断点击外部是否关闭,也不用担心滚动穿透,很多天生的坏毛病都消失了。
使用中的几个小坑
popover虽然方便,但也不是完全没坑。我把自己踩过的提醒你一下:
第一,popover元素默认自带边框、内边距等样式,并且它永远不会主动display:none。如果你要隐藏它,该加display:none?千万别。建议直接使用它的属性切换,浏览器会处理。如果想初始化时不显示,就不要加open属性,它默认是隐藏的。
第二,想用showPopover()打开一个已经在显示状态的popover,浏览器会抛异常。所以最好先判断一下element.matches(':popover-open'),再决定调用hide还是show。
第三,popover元素不要放在带有overflow: hidden的父容器里。虽然top layer不受overflow影响,但如果你用了transform或者filter之类的属性,父容器会创建一个包含块,干扰定位。这个问题很隐蔽,我花了好几个小时才找到原因。
结尾:别急着用框架
我并不是说UI库没有存在价值,只是对于轻量级项目,原生HTML的能力已经越来越强。现在很多组件我们真的可以用几个属性替代。我甚至幻想未来写前端做浮层只需要popover加上popovertarget,连class都不用写。
如果你还没试过popover,真的建议打开浏览器,用十分钟时间跑一遍上面的例子。这种不需要依赖任何包就能搞定交互的感觉,很久没有体验过了。

