前几天朋友发来一段代码,问我这个弹层能不能用纯 HTML 实现。我点开一看,里面又是 position: fixed,又是 z-index,还要用 JS 控制 display 切换。我就跟他说,那你不如试试看 Popover API 了。他半信半疑。
于是我把最近在 Chrome 上调试好的一个案例甩给他,他看完直接说要把项目里的弹窗组件全部换掉。说实话,我也刚玩这个新特性没几天,但确实觉得它是个早晚要普及的东西。今天就把我这个摸索过程写成一篇实战记录。
Popover 是什么?先解决最原始的痛点
做网页弹层的时候,最麻烦的是三件事:点击按钮显示,点击空白处关闭,以及让弹层浮在最上层。以前没有 Popover 的时候,你得自己写一堆 class,或者依赖 Bootstrap 那样的组件库。如果项目本身不想引入庞大的 JS,那就只能自己撸。
现在有了 Popover API,这三个问题直接被浏览器原生解决。你只需要给某个元素加一个 popover 属性,它就成了一个弹层元素。再用一个带有 popovertarget 属性的按钮指向它,点击按钮的时候弹层就会自动显示/隐藏。就两行 HTML,不用写一行 JS。
下面是最最基础的一个例子:
<button popovertarget="my-popover">点我弹出</button>
<div id="my-popover" popover>这是弹层内容,点外面就能关掉</div>
你把这些代码放到一个 HTML 文件里,在 Chrome/Edge 115 以上的浏览器打开,就会发现点击按钮后弹层出现,点击弹层以外的区域自动消失。这就是 Popover 的默认模式(auto)。
深入 Popover 的显示隐藏机制
你肯定注意到了 popover 属性默认值。实际上它有三种状态:
- auto:默认值,也是最常用的。弹层显示后,点外面、按 Esc 键、或者点击其他 popover 触发按钮,都会自动关闭。
- manual:需要手动控制关闭,点外部不会自动关,按 Esc 也不关。适合做那种需要用户确认的操作框。
用法也很直接:
<div id="manual-pop" popover="manual">这种弹层只能手动关</div>
带 manual 的弹层,点击空白不会收回去,只有再次点击触发按钮,或者通过 JS 调用 hidePopover() 才能收起来。
想要用 JS 控制怎么办?API 提供了三种方法:
const pop = document.getElementById('my-popover');
pop.showPopover(); // 显示
pop.hidePopover(); // 隐藏
pop.togglePopover(); // 切换显示/隐藏
你可以挂在按钮的 click 事件里,但实际上原生按钮用 popovertarget 已经够了。只有当你需要同时联动别的逻辑时才需要写 JS。
另外 Popover 还提供了相关事件:
pop.addEventListener('toggle', (event) => {
if (event.newState === 'open') {
console.log('弹层打开了');
} else {
console.log('弹层关闭了');
}
});
这个 toggle 事件特别适合做数据初始化或者埋点统计。不需要自己监听 class 变化就能知道弹层状态。
弹出层到底能放什么?嵌套玩法了解一下
Popover 可以嵌套。比如你有一个设置弹层,里面又有一个“更详细的设置”按钮,点击后出来子弹层。默认 auto 模式下,子弹层打开后,父弹层不会自动关闭;点击子弹层外部、父弹层内部时,子弹层会关闭,但父弹层不会。
看个代码结构:
<button popovertarget="parent-pop">打开设置</button>
<div id="parent-pop" popover>
<h3>设置面板</h3>
<button popovertarget="child-pop" popovertarget-action="show">更多选项</button>
<div id="child-pop" popover>这里是子弹层</div>
</div>
这里有个 popovertarget-action 属性,它的值可以是 show、hide 和 toggle。默认是 toggle,也就是点来点去切换。如果你希望按钮只负责显示,那就写 show,点它不会把它关掉。
嵌套 Popover 在移动端很实用。因为移动端屏幕小,点击进去看详情,返回后保留上一级,体验很顺。
和对话框(Dialog)有什么区别?
如果你用过 <dialog> 元素,可能会问两者是不是同一回事。其实它们很多行为很像,但定位不同。
Dialog 通常用来做模态框,它会改变页面的语义和交互,比如阻塞页面内容,配合 showModal() 使用可以实现背景不可点击。而且 Dialog 自带 ::backdrop 伪元素用来显示遮罩层。
Popover 更像是一种非模态的浮动层,例如浮层菜单、消息提示、下拉气泡,它默认不阻塞页面,点击外部自然关闭。如果你想给 Popover 添加遮罩层,可以用 CSS 的 ::backdrop 伪元素,不过 Popover 的默认背景遮罩是透明的,需要你自己去画。
那什么时候用 Dialog 什么时候用 Popover?我的建议是:如果是必须用户确认的流程,比如删除确认,用 Dialog;如果是无关轻量的悬浮信息,用 Popover。
给 Popover 加点样式:自定义箭头和小三角
既然 Popover 本质就是一个浮层元素,那样式完全由你控制。我想做的是一个聊天设置小气泡,顶上有一个小箭头指向上面的按钮。只需要把 Popover 的背景和伪元素搞定。
看实际代码:
<button class="avatar" popovertarget="tip">头像</button>
<div id="tip" popover class="message">这是一个带箭头的提示气泡</div>
然后我写对应的 CSS(这里为了展示只给出核心部分),用 ::before 画小三角:
.message {
position: relative;
background: #333;
color: #fff;
padding: 12px 18px;
border-radius: 8px;
border: none;
}
.message::before {
content: "";
position: absolute;
bottom: 100%;
left: 20px;
border: 8px solid transparent;
border-bottom-color: #333;
}
不过这需要给 Popover 设置一个合适的位置。默认情况下 Popover 出现在视口中心往下一点的位置,并不像 Tooltip 那样依附在触发按钮旁边。如果要做正统的 Tooltip,可以用 CSS 的 anchor positioning 技术,目前还在实验阶段,但 Chrome 里已经可以用了。
简单来说,你可以给触发按钮加一个 anchor="anchor-1",然后弹层设置 position: fixed; anchor-default: --anchor-1,再配合 top: anchor(bottom) 等方法定位。
不过这个特性还没完全普及,我建议先用普通定位或者 JS 定位,等浏览器兼容性稳定后再深入使用。
移动端适配和浏览器兼容性
目前 Chrome、Edge、Safari 16.4+、Firefox 125+ 都已经原生支持 Popover 了。对于不支持的老浏览器,我找了两个降级方案:
第一,用 @supports 判断,如果浏览器支持 popover 属性,就启用原生行为;不然还是用传统的隐藏方式(display: none)然后通过 JS 控制。但这个写起来并不省事。
我当时做项目时直接加了小的 polyfill,不过说实话,如果你只在支持的环境里使用,体验很顺滑。如果项目对老浏览器有要求,还是老老实实用组件库吧。
移动端上,Popover 的表现很接近原生应用,特别是打开和关闭的过渡动画可以用 CSS transition 来实现。但注意 CSS transition 作用于 Popover 的时候,需要设置块级上下文,否则动画不生效。
下面是让弹层加一个淡入效果的样式思路:
[popover] {
transition: opacity 0.2s ease;
opacity: 0;
}
[popover]:popover-open {
opacity: 1;
}
不过需要注意的是,默认弹出的 Popover 会立即出现在顶层,用 :popover-open 伪类可以选中当前处于显示状态的 Popover 元素。这个伪类在写样式时很方便。
一个完整的实际案例:番茄钟的自定义提示
正好最近我在做一个番茄钟小工具,里面用到了 Popover 来显示自定义快捷时间。我把整个关键代码精简了一下,你可以直接把它保存成 HTML 文件运行试试。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>番茄钟快捷设置 - Popover 案例</title>
</head>
<body>
<button popovertarget="quick-set" class="open-btn">快捷设置</button>
<div id="quick-set" popover class="quick-panel">
<p>选择工作时间</p>
<button class="time-btn" onclick="setTime(25)">25 分钟</button>
<button class="time-btn" onclick="setTime(45)">45 分钟</button>
<button class="time-btn" onclick="setTime(90)">90 分钟</button>
<button class="close-btn" popovertarget="quick-set" popovertarget-action="hide">关闭</button>
</div>
<p id="current">当前定时:25 分钟</p>
<script>
function setTime(minutes) {
document.getElementById('current').textContent = '当前定时:' + minutes + ' 分钟';
document.getElementById('quick-set')?.hidePopover();
}
</script>
<!-- 这里刻意不写 style,为了演示,你可以自己加上样式 -->
</body>
</html>
在这个案例里,你用原生 Popover 取代了之前需要 JS 控制的弹层,用 popovertarget 按钮打开,用 hidePopover 在选项点击后关闭,非常干净。如果放在过去,你得给按钮绑定事件,还要给弹层加个类名去切换 display,代码至少多五倍。
当然上面的展示没写 CSS,实际页面里你可以自己加一点内边距、边框和阴影,让它看起来像一个正常的卡片。
最后聊聊让我觉得惊喜的地方
Popover 给我最大的好感,不只是省代码。它让“弹层”的语义变得清晰,浏览器会自动管理动画、焦点、点击外部关闭等行为。而且它是原生 API,以后不管用 React、Vue 还是原生 JS,都能直接使用,不需要再依赖第三方库。
当然它也有一些目前限制,比如没有内置的模态遮罩,对 Tooltip 定位的支持还没完全落地。但以我对浏览器发展的经验,过不了多久,这些都会成为历史。到那时可能我们连弹层组件库都不需要了。
如果你也喜欢折腾新特性,可以打开浏览器控制台,输入 document.createElement('div').popover,看看返回的值是否存在。如果返回空字符串,说明你的浏览器已经支持。要是返回 undefined,那就去更新浏览器或者考虑 polyfill。
以后我做网页会优先尝试 HTML 原生方案,有时候真的,浏览器已经给得够多了。

