上周刷浏览器更新日志,看到 Popover API 已经成为 Baseline 了。以前想做个气泡菜单,要么自己用 JS 定位再加全局点击监听,要么引入 Popper.js,现在浏览器直接给了原生的 popover 属性,用起来还挺顺手。本来没抱太大期望,结果用它做了一个“点赞气泡卡片”之后,我默默删掉了几十行监听事件。
这篇文章不是翻译文档,而是拿着一个实际例子带你把玩一下这个属性。全部代码只有 HTML,连 JS 都只用了半行(用于关闭按钮)。如果你还没试过,可以自己复制到一个 .html 文件里跑一下。
Popover 是什么不多说,直接看怎么用
最简单的用法是给一个元素加上 popover 属性,它默认隐藏。再给一个按钮加 popovertarget 属性指向那个元素的 id,点击按钮就能实现开关。
<button popovertarget="tip">点我</button>
<div id="tip" popover>
我是默认气泡
</div>
就这么简单,点按钮弹出来,点气泡外部它就自动关闭。如果你的 popover 属性值是 "auto"(默认就是 auto),它还会同时只显示一个,点别的地方就关,像原生下拉菜单。
我想做个“点赞后显示操作气泡”
最近在弄一个文章卡片,希望用户点“点赞”图标的时候,浮出一个小气泡,里面有“情绪”选项,比如“开心”、“赞”、“666”。以前我得用 setTimeout、动态创建 DOM、再绑个 document 点击事件。现在有了 popover,感觉清爽了。
先看一下我最终的结构。
<article id="post-card">
<h3>今天也是元气满满的一天</h3>
<p>早起做了30个俯卧撑,然后喝了一大杯冰美式…</p>
<footer>
<button id="like-btn" popovertarget="like-pop" style="font-size: 18px;">👍 点赞</button>
<div id="like-pop" popover>
<div class="pop-actions">
<button data-emoji="😄">开心</button>
<button data-emoji="👍">纯赞</button>
<button data-emoji="🔥">上头</button>
</div>
</div>
</footer>
</article>
注意这个按钮上没有写 JS,浏览器会帮我切换 popover 的显示状态。但问题来了:我希望点击气泡里面的某个情绪按钮后,把点赞按钮上的表情换成对应的 emoji,并且把 popover 关掉。
让气泡里的按钮替“点赞”按钮换表情
这个需求就需要一点点 JS。不过也是简单到没朋友。因为 popover 元素在 DOM 里正常存在,我直接监听它的点击事件就行。
<script>
const likeBtn = document.getElementById('like-btn');
const likePop = document.getElementById('like-pop');
likePop.addEventListener('click', (e) => {
const emoji = e.target.dataset.emoji;
if (emoji) {
likeBtn.textContent = emoji + ' ' + e.target.textContent.trim();
likePop.hidePopover(); // 原生方法关闭
}
});
</script>
这里用到的 hidePopover() 是 popover 元素自带的 JS 方法,比手动取消一个 class 舒服多了。当然你也可以写成 likePop.setAttribute('popover', ''),但都不够原生。
更进一步:让气泡的位置靠近按钮
默认弹出的 popover 是出现在页面视口的正中央,就跟 alert 一样。但实际产品里,气泡应该贴在点赞按钮附近。这个当前用纯属性还做不到,需要配合 CSS 的锚定定位(Anchor API)。
目前 Chrome 已经支持了锚定定位,但为了不跑偏,我先用个笨办法:把 popover 放进按钮的父容器,然后通过 CSS 去偏移。虽然不和“锚定定位”那么高级,但效果还行。
但咱们这文章说好不用样式,那就不展示。只说一下:你可以在 style 里给 #like-pop 设置 position: absolute; inset: unset; margin-top: 8px; 之类。等下,popover 元素默认是放在顶层(top-layer),它不在正常的文档流里,所以 absolute 定位相对于视口。还是用锚定定位靠谱。
说到锚定定位,其实 Popover API 的官方示例就是配合 anchor 属性使用的,但这又是一块内容。我这里不想把文章搞太长。不过还是可以给你看一个关键属性的雏形:
<div id="like-btn" anchor="like-pop">...</div>
<div id="like-pop" popover anchor="like-btn">...</div>
现在很多浏览器还没完全统一,真要在生产环境用,还是老老实实配个 transform 吧。
额外惊喜:popover 自动支持滚动关闭和取消关闭
做完了之后,我注意到一个细节:当 popover 弹出时,按 Esc 它会自动关闭。而且当你点击 popover 外部区域,它也会自动关闭。这在用户习惯上非常友好,完全不需要我再额外监听。
但有时候我们不想让它点击外部关闭,那就把 popover 的值设为 manual。手动模式会让它只能通过调用 showPopover() 或 hidePopover() 来控制,适合做那种“必须明确选择”的确认框。
完整的“点赞气泡”HTML文件
下面这个文件你可以直接保存为 .html 运行,体验一下完整的效果。我没写任何样式,所以可能有点朴实无华,但交互都是好的。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo:点赞气泡</title>
</head>
<body>
<article style="max-width: 400px; margin: 50px auto; border: 1px solid #ddd; padding: 16px;">
<h2>周末去爬山</h2>
<p>早上六点出门,爬山三小时,回来睡了一下午,感觉这才叫生活。</p>
<hr>
<button id="like-btn" popovertarget="like-pop">👍 点赞</button>
<div id="like-pop" popover>
<div style="display: flex; gap: 8px;">
<button data-emoji="😄">开心</button>
<button data-emoji="👍">纯赞</button>
<button data-emoji="🔥">上头</button>
</div>
</div>
</article>
<script>
const likeBtn = document.getElementById('like-btn');
const likePop = document.getElementById('like-pop');
likePop.addEventListener('click', (e) => {
const emoji = e.target.dataset.emoji;
if (emoji) {
likeBtn.innerText = emoji + ' ' + e.target.innerText.trim();
likePop.hidePopover();
}
});
// 顺便展示一下手动控制的玩法:长按按钮强制显示气泡
likeBtn.addEventListener('contextmenu', (e) => {
e.preventDefault();
likePop.showPopover();
});
</script>
</body>
</html>
我在这里给按钮加了个右键菜单事件,长按右键也会弹气泡,这是利用 showPopover() 方法。你如果不想要,删掉那段 JS 就行。
有些坑还是得记录
用下来发现几个问题:
- popover 会默认带上浏览器自带的边框和背景。 如果你没加任何 CSS,它可能长得很像老式弹窗。不过这正是定制的好起点,至少比之前用 div 模拟的强。
- 多个
popover="auto"之间会互相顶掉。 这其实是特性,但如果你不小心把对话框也设成 auto,打开一个就会关掉另一个,有点尴尬。 - popover 里放
<form>提交会把气泡关掉吗? 我试了一下,提交后不会自动关闭,需要手动关闭。这个要看场景。 - 移动端点击外部关闭有点难触发。 因为移动端没有 hover,但点击空白区域会自动关闭,感觉还行。
收尾
Popover API 的出现让很多常见交互变得“傻瓜化”。虽然现在很多人还在用框架做气泡,但原生能力迟早会被更多开发者用起来。如果你也遇到类似“点击某个元素弹出小卡片”的需求,不妨先别急着引第三方库,试试这个原生属性。也许会有惊喜。

