以前想给页面加个悬浮层、下拉菜单或者提示气泡,基本绕不开 JavaScript。先弄个 div 绝对定位,监听 click 切换 display,还得判断点外面关闭,一套下来少说几十行代码。最近 Chrome 和 Safari 都陆陆续续支持了 HTML 原生 popover 属性,等我上手一用,只能说真的回不去了。
Popover 这玩意儿用起来就像 input 标签一样自然,它不需要你导入任何库,也不需要写一大堆事件管理。这篇文章就讲讲我实际用下来的几个场景,全是干货。
1. popover 是啥?看个最简单的例子
popover 让你可以把一个元素变成“浮层”。默认情况下它是隐藏的,你得给个按钮用 popovertarget 指定目标,然后一点,浮层出现,再点一下,浮层消失。按一下页面其他地方,浮层自己也关了。零 JS。
看代码:
<button popovertarget="my-popover">点我显示浮层</button>
<div id="my-popover" popover>
<h3>这是一段浮层内容</h3>
<p>不需要任何 JS 的控制,原生就支持关闭逻辑。</p>
</div>
你在浏览器里打开这段代码,点按钮会出现浮层,点是别处它自动消失。也可以按 esc 关闭。这感觉就像框架给封装好了一样,但它就是原生 HTML。
2. 显隐控制的三种姿势
popover 属性最基础,但你可以通过命令 API 任意控制显隐。有三种方式:
- 声明式:用
popovertarget指向目标元素的 id,可带popovertargetaction="show"或hide"。 - 脚本调用:拿元素调用
.showPopover()、.hidePopover()或.togglePopover()。 - 自动属性:元素本身加
popover属性,点外部自动关闭,这是默认行为。
比如我想要一个点击按钮弹出,但再点按钮不关闭,点外部关闭,那你可以这样:
<button popovertarget="tip1" popovertargetaction="show">显示提示</button>
<button popovertarget="tip1" popovertargetaction="hide">隐藏提示</button>
<div id="tip1" popover>我只会听按钮的</div>
这个很实用吧,以前你得写个变量存状态,现在属性直接搞定。
3. 设置浮层的位置和边距
popover 毕竟是浮层,位置不一定跟着按钮走,它默认是放在页面中央偏上?实际上它由浏览器决定。为了统一,我一般会加个小样式让它看起来像个卡片。
虽然文章不让用 style 标签,但那是为了页面展示干净,在真实项目里你写 CSS 就行。通常我这样写:
[popover] {
border: 1px solid #d0d0d0;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
width: 240px;
}
如果你不想依赖 CSS,也可以用内联样式。不过我更推荐把浮层当普通元素,用 :popover-open 伪类来设计打开状态。
比如让浮层出现的时候带个渐变动画:
[popover]:popover-open {
animation: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
4. 做一个真正的下拉菜单
下拉菜单是最常见的浮层使用场景。用 popover 做起来我的思路是:按钮用 popovertarget,菜单本体加 popover,然后菜单里放链接。点击链接后页面跳转,浮层自动关闭吗?它不会自动关,不过你可以给链接加个点击事件隐藏。但纯 HTML 也能做到,只要把菜单用一个 form 包起来,里面的 button 写 popovertarget 和 popovertargetaction="hide"。
话不多说,看代码:
<div class="menu-wrap">
<button popovertarget="menu" popovertargetaction="toggle">菜单按钮</button>
<div id="menu" popover>
<a href="/profile" rel="external nofollow" >个人资料</a>
<a href="/settings" rel="external nofollow" >系统设置</a>
<button popovertarget="menu" popovertargetaction="hide">关闭菜单</button>
</div>
</div>
这里我加了个关闭按钮,但其实用户点外面也可以关。菜单里的链接点击后浏览器会跳转,导致整个页面刷新,所以浮层也自然没了。如果你用 SPA 路由,那可能还是要写点 JS 做拦截,但至少都不用建 class。
5. 替代手风琴和弹层:popover 与 <dialog> 的取舍
很多人刚接触 popover 就会想:这跟 <dialog> 标签有什么区别?我一开始也懵,后来对比完发现各有侧重。
dialog 适合模态框,需要焦点锁定,按 Esc 关闭,通常配合 showModal() 使用。它的样式和语义更偏传统弹窗。
popover 是非模态的,它不锁定焦点,更像一个“气泡卡片”,适合轻量浮层。你不能把它当全屏遮罩弹窗用,最多就是个提示层或工具菜单。
我通常这样选择:
- 如果是要用户必须确认或者输入的,用 dialog。
- 只是展示辅助信息,比如“什么是积分”这种小气泡,用 popover 太合适了。
用 popover 写 tooltip 其实很顺手,比如:
<button popovertarget="tip2">问号图标</button>
<blockquote id="tip2" popover>积分每个月月底清零哦。</blockquote>
6. 嵌套浮层?不行,但可以这样绕
popover 默认不允许嵌套。就是说你一个浮层里面再放一个 popover 触发器,点内层浮层时外层会自动关闭。官方说多个 popover 同时显示时,后出现的会关闭之前的。这在某些复杂场景有点局限,比如需要一个层级分明的菜单。
但老办法总是有的。你可以在内层浮层上使用 e.stopPropagation() 阻止冒泡,或者在触发器上加上 popover 属性时设置 manual 状态。
popover 还可以设置 manual 关键字,让它不自动关闭。用这个可以做出多级交互:
<div id="pop1" popover="manual">
<button popovertarget="pop2">打开子菜单</button>
<div id="pop2" popover="manual">我是子菜单</div>
</div>
手动模式不会因为点击外部关闭,必须调用 hidePopover() 或者绑定 action 来关。这样一来嵌套交互就自由了。
7. 兼容性:这情况有点乱
我之所以说“今日有热度”,是因为兼容性刚刚开始全面。在写这篇之前,我查了一下:Chrome 114+、Edge 114+、Safari 17+ 都已经支持 popover 了,Firefox 在 125 版本也默认支持了。也就是说到 2024 年,现代浏览器基本都能跑。如果你想兼容老浏览器,也可以做个简单降级:把浮层默认设为普通隐藏块,然后检测不支持时用 class 切换。
@supports (interpolate-size: allow-keywords) {
/* 这东西跟 popover 没关系,但可以用 @supports selector(:popover-open) */
}
@supports selector(:popover-open) {
/* 浏览器支持原生 popover */
}
@supports not selector(:popover-open) {
/* 降级方案 */
}
不过说真的,2024 年了我也不太想考虑 IE 的破事。popover 这种新特性的收益是很实在的,用起来吧。
8. 真实项目怎么用它替代旧代码
我上个月把一个老 Vue2 项目里的所有下拉菜单换成了原生 popover。UI 结构不变,就是删掉了一堆 toggle 函数,然后在模板里加 popover 和 popovertarget 属性。旧项目里那些 `@click=”show = true”`、`@click.stop`、`document.addEventListener(‘click’, …)` 的代码全部见鬼去了。
另外一个好用的地方是:在表单里做输入提示。以前需要写个 span 隐藏,然后鼠标事件切换。现在直接挂个 popover,按钮一戴就完事。
但有一点要特别注意:popover 是“顶层”元素,它会盖在整个页面内容之上,但 z-index 有时不太好控制。如果你页面里有 position: fixed 并且设置了很大的 z-index,可能还会压不住。不过一般浮层场景没这种问题。
9. 几个容易踩的细节
折腾这么久,我总结一下容易踩的坑:
- 元素必须
display: none吗?不用,加了popover属性默认就是隐藏的,但是如果你定义了display: block内联样式,可能破坏默认行为。所以别在标签上写样式,放 CSS 里。 - 按钮和浮层不在同一个父级也可以,id 全局唯一就行。
popover属性只对 HTML 元素有效,对 SVG 元素无效。- 用
popovertarget时,不能写成popovertarget="my popover",id 不能有空格。
10. 动手写个完整 demo
说来说去不如一个大杂烩。我把上面的一些点合在一起,写个几乎不用 JS 的“通知”组件:
<!DOCTYPE html>
<html>
<body>
<button popovertarget="notice" popovertargetaction="show">查看新通知</button>
<div id="notice" popover="manual" style="max-width: 320px;">
<h3>新通知</h3>
<p>今天晚上服务器维护,部分功能暂不可用。</p>
<button popovertarget="notice" popovertargetaction="hide">我知道了</button>
</div>
</body>
</html>
这里 manual 模式让手动关闭,点外部不会隐藏。其实用默认的 popover 模式也可以,然后“我知道了”按钮就多余了。看个人需求。
如果你想要一个点按钮出现,再点一下消失的下拉菜单,就不需要任何手动关闭按钮,直接这样:
<button popovertarget="share-menu">分享</button>
<div id="share-menu" popover>
<ul>
<li>微信</li>
<li>微博</li>
<li>复制链接</li>
</ul>
</div>
点按钮,菜单显示;再点按钮,菜单隐藏。点旁边,菜单也会隐藏。不需要写一个 toggle 函数,这就是原生。
11. 我跟它配合得很好的组合技
有一说一,popover 最适合和 SVG 图形一起做图例说明。之前做图表项目,图例旁边需要问号图标,悬浮说明某个指标含义,用 popover 轻松搞定。鼠标悬浮也只需要在按钮上监听 mouseenter 和 mouseleave 调一下 showPopover() / hidePopover() 就行。
比如:
button.addEventListener('mouseenter', () => popover.showPopover())
button.addEventListener('mouseleave', () => popover.hidePopover())
这可比自己定位和改 display 舒服多了,而且它拥有原生语义,屏幕阅读器也能正确读出是浮层。
12. 总结一下,这玩意值得用吗
反正我现在新项目里能不用 JS 做浮层的地方,全部优先用 popover。代码可读性高了好多,也少了很多“点这里开关”的逻辑。对我来说,它最大的价值不是省了 js,而是让 HTML 结构本身就表达了UI的状态。
这篇文章不是让你抛弃所有前端框架,只是希望大家知道浏览器已经提供了一套简洁的浮层基础。写小的互动效果、项目原型,或者构建一些轻量组件,直接拿原生 popover 来用,真的很顺手。
以后看到别人用一堆 JS 控制显示隐藏的时候,你可以默默刷个 popover 的属性给他看,也算种草了。

