HTML popover属性:不用写一行JavaScript就能做弹层菜单

2026-09-07 0 336

上周做一个小活动页面,需求很简单:点击一个按钮,出来一个小浮层,里面有几个快捷操作。搁在以前,我第一反应是写个div,绝对定位,加个class控制显示隐藏,再用JavaScript监听点击和点击外部关闭。做了这么多年前端,这活儿闭着眼也能干。

但是,今年在检查同事提交的代码时,发现他写了一个我平时不太常用的属性——popover。浏览器原生支持的弹层,不用自己处理z-index、focus、点击关闭、事件冒泡命中。

当时我第一反应是“这玩意儿靠谱吗?”于是去查了一下支持情况。好家伙,现代浏览器在2024年左右已经基本全绿了。我半信半疑在项目里试了一把,结果整个弹层的代码量减少了大约70%。还真不是夸张。

今天就把我折腾这个原生popover的过程写下来,给还在用老办法做弹层的同学一个参考。

先说popover是什么

HTML里给元素加一个popover属性,它就被浏览器当作一个弹出层元素来看待。默认情况下,这个元素是隐藏的。它可以和其他几个专属API配合使用,比如使用showPopover()方法,或者使用更简单的声明式调用方式——用popovertarget属性挂在按钮上。

最简单的用法是这样:

<button type="button" popovertarget="tip" popovertargetaction="toggle">显示快捷键</button>

<div id="tip" popover>
    <p>按 Ctrl+K 打开命令面板</p>
</div>

注意button上的两个属性:popovertarget指向弹层id,popovertargetaction有三个可选值,showhidetoggle。假如不写popovertargetaction,默认就是toggle。点一次显示,再点一次关闭。

浏览器对弹层的“轻触外部关闭”原则很严格。popover元素默认就是“轻量级弹层”,只要点击弹层以外的区域,它就会自动关闭。这就是很多UI组件库里“点击遮罩关闭”的功能,现在原生实现了。

这种声明式的写法,是真的不需要写一行JavaScript就能实现交互。而且对于多个按钮控制同一个弹层也没问题。

和传统的hidden或display切换有什么区别

以前我们用hidden或者display:none做隐藏元素,再用class控制出现,还需要管理aria-expanded属性。而原生popover在被隐藏时,浏览器自动把它从无障碍树里移除。不会像div那样,虽然看不见,但是读屏软件却可能读到里面的文字。

这一点在无障碍上带来的收益应该是意外的,但确实好用。当然如果你用的是对话框dialog元素,那是另外一套机制,这里说的是popover。

因为popover元素默认带有浏览器预设的样式效果,比如它会出现在最上层,背景会有半透明的遮罩?等一下,其实并没有强制遮罩。你可以通过CSS自定义背景、边框、位置。默认的样式为:position: fixed,元素宽度由内容决定,并且自带一个很淡的阴影。在不同浏览器里长得不太一样,但都不算丑。

弹层的最上层性质,不用再写一个很高的z-index。浏览器会自动把popover元素置于顶层,就好比播放视频全屏那样。

/* 在不懂样式时,popover看起来就像固定定位的卡片 */

配合CSS的::backdrop伪元素,还能做一层半透明遮罩。

#tip::backdrop {
    background: rgba(0, 0, 0, 0.3);
}

不过这里的遮罩不会挡住外面的交互,它只是一个视觉装饰。想通过点遮罩关闭弹层,还是用“点击外部关闭”的原生行为,所以体验还好。

使用完,真觉得遇到问题还是要回退到JS

原生popover虽然很方便,但并不是万能的。有些小细节需要自己额外处理。我举几个例子。

期望的交互奇奇怪怪?自己监听toggle事件

popover元素有一个beforetoggle事件和一个toggle事件。当弹层显示/隐藏的时候触发。我们可以用这些事件来做一些额外操作。比如记录埋点,或者在打开弹层时把焦点设置到输入框。

const pop = document.getElementById('tip');
pop.addEventListener('toggle', (event) => {
    // event.newState 是 'open' 或 'closed'
    if (event.newState === 'open') {
        console.log('弹层打开了');
    }
});

浏览器对这个支持得很好。在刚开始实验的时候我忘了加事件监听,结果弹层出来以后焦点还在按钮上,键盘用户按下空格,又触发了开关。若想控制焦点,可以在toggle后手动调pop.querySelector('input').focus()

不要让嵌套的popover把自己关了

默认的“点击外部关闭”机制,在弹层内部放置一个子级popover时,可能会发生奇怪的“连锁关闭”。因为内部弹层也属于外部?不,内部弹层应该包裹在同一个父popover元素里,或者用ancestor规则去避免。

popover有“祖先”的概念,具体来说,一个popover元素内包含另一个popover,点击内部弹层时外部弹层不会关闭。这比较好理解。但如果两个弹层是兄弟元素,点击一个弹层内的区域,却会触发另一个弹层的关闭。因为兄弟区域被当成“外部”。

所以遇到类似“点击一个按钮出现tooltip,点击tooltip里的另一个按钮再出现一个menu”这样的UI,需要小心使用。一种办法是嵌套,让内部弹层成为外部弹层的子孙节点,就像下拉菜单的二级菜单。

下面是简单的嵌套结构:

<div id="outerPop" popover>
    <button popovertarget="innerPop" popovertargetaction="toggle">更多选项</button>
    <div id="innerPop" popover>
        <a href="#" rel="external nofollow" >编辑</a>
    </div>
</div>

用这种结构,点击内部弹层的时候不会关掉外层弹层。

默认行为关闭不了,用CSS就能让它常显

popover元素可以被强制一直显示吗?可以使用JavaScript调用showPopover(),但如果用户点击外部还是会自动关闭。如果想要把它当成一个“模态框”(模态对话框)来一直锁住外部,不让点外部关闭,就需要配合一些逻辑,比如在按钮上click后,立刻给弹层加一个监听,或者用一个新的方法叫showModal()。不过后一个方法是dialog元素的。

现实中我做纯提示tooltip时,比较少出现需要强制关闭的场景。但如果真要做右键菜单那种,完全不带“轻击外部关闭”的模态,可以考虑:将弹层打开后,在文档根部监听pointerdown,如果target在弹层以外,阻止这个事件,然后去做别的。但这样就会跟原生popover的关闭混在一起。

所以我的取舍是:能符合默认光照就尽量用popover;非要复杂模态交互,还是老老实实使用dialog + showModal。

结合动态内容改造成提示气泡

有些同学喜欢用CSS伪元素给小气泡画一个三角箭头。popover元素完全支持。我们对一个popover加一个class就可以定义它的位置,比如把它定位在按钮下方。比较麻烦的是定位的基准坐标不是自动的。popover默认是相对视口居中?其实不是,它的默认position是fixed,初始位置可能是在视口中央,但因为没有left/top属性,具体位置依赖于元素的自然文档流?其实它离开了文档流,固定定位,没有设置left/top就会停留在原本它在DOM里的位置附近?也不太确定,不同浏览器表现略有不同。最常用的定位方法还是JS计算按钮坐标后赋left/top。

这算是一个痛点。比如希望点击一个按钮后,popover显示在按钮正下方。没有原生锚点定位,你得自己算。我的处理办法是给按钮绑定一个click,然后用getBoundingClientRect()获取按钮位置,再设置popover的left和top。千万不要忘了把popover的margin给重置一下。

btn.addEventListener('click', () => {
    const rect = btn.getBoundingClientRect();
    const popoverEl = document.getElementById('tip');
    popoverEl.style.left = rect.left + 'px';
    popoverEl.style.top = (rect.bottom + 8) + 'px';
    if (!popoverEl.matches(':popover-open')) {
        popoverEl.showPopover();
    }
});

这里使用CSS伪类:popover-open可以判断弹层是否显示。按逻辑,先定位再打开,基本就稳了。

如果页面滚动,或者浏览器窗口尺寸改变,popover的位置不会跟着更新,你需要额外监听scroll和resize调整位置。这种只要popover已打开时的实时跟随定位,肯定还得写JS。原生没有弄这个,它主要解决的是显示隐藏和层叠管理。

在项目中批量替换旧的div弹层

我们项目里原先有几十个“点击出现气泡”的交互。老代码清一色的结构是:一个div absolute,一个按钮click切换class,再做了document中click事件监听来关闭。

重构前,需要看每一个弹层和按钮的具体逻辑。有的弹层内容里有动态数据,事件都是委托到父级。直接把那个div改成popover属性以后,原来绑定在div上的事件可能会受影响吗?放心,因为事件绑定在元素上,不涉及显示方式,不影响。

把原来控制显示隐藏的代码删掉,只需保留popovershow相关的showPopover调用就会自动出现。原来点击外部关闭的逻辑可以直接删除。因为原生popover已经做了这件事。另外,将原来在div上的某些aria属性也删掉,避免跟原生产生冲突。

有一个小坑:popover元素不能嵌套在另一个popover元素内,除非这两个popover之间是祖先关系。假如你有一个menu按钮,menu里面又有子菜单(用一个列表),那就需要上面的嵌套结构,仅支持嵌套popover,而不是把popover放在其他div里。

具体来说,如果列表中的每一项均包含一个popover,那这些popover不能互相包裹。如果它们不是父子关系,点击其中一个会导致另一个关闭。所以我们改造的时候,对于这种嵌套下拉,需要专门处理。

真的一个脚本都不用写?不全是

如果只是调用toggle属性就能弹出隐藏,那你需要HTML按钮上写popovertarget。比如一个按钮就让弹层显示,两个按钮分别控制显示和隐藏。这样不用JS,可以实现基本交互。

但是如果想要点击弹层内部的某个链接后自动关闭弹层,那原生不支持。比如用户点了气泡里的确认按钮,期望整个menu消失。虽然点击按钮时,鼠标落在弹层内部,不会触发外部关闭,所以还需要JS主动hidePopover。

confirmBtn.addEventListener('click', () => {
    popoverEl.hidePopover();
});

这种场景躲不开JavaScript。所以很多时候还是在外面包了一层简单的监听器。

做一个通用的帮助气泡组件

我能想到最合适的应用是放在帮助提示上。在每个设置项旁边放一个问号图标,点击弹出一段说明文字。点击页面其他位置自动关闭,完全符合默认轻量popover行为。

我封装了一个很小的helper方法:

function attachHelp(buttonId, contentHtml) {
    const pop = document.createElement('div');
    pop.innerHTML = contentHtml;
    pop.setAttribute('popover', '');
    document.body.appendChild(pop);

    const btn = document.getElementById(buttonId);
    btn.setAttribute('popovertarget', pop.id);
    btn.setAttribute('popovertargetaction', 'toggle');
}

pop.id需要唯一生成,临时写没问题。只要把popover挂到body下,它的层级天然高于页面其他元素,不会跟父级overflow冲突。这比使用tooltip的父级relative方案省心许多。

在微信小程序开发里虽然不能用这些API,但web端搞H5项目时用它真的很迅速。

浏览器兼容性,还是要啰嗦一句

如果是做内部管理系统,用户都在Chromium系浏览器上,放心用。如果是做电商大促,需要兼容旧版Safari,就得提前加个判断。因为Safari直到16.4才开始支持popover,更老的版本则完全失效。我给这个帮助按钮做了回退处理:如果!('showPopover' in HTMLElement.prototype),就退回原来的class切换方式。

这也就是渐进增强。现代浏览器体验原生效果,老浏览器照常用老办法,不会功能缺失。

if (typeof HTMLElement.prototype.showPopover !== 'function') {
    // fallback: 加class控制显示隐藏
}

反正当前2025年,主流移动端safari和chrome都支持得很好。uni-app里的web-view组件也支持。

最后放一个完整例子:消息气泡

下面我在页面里写一个能直接跑的完整案例,没有多余样式干扰,你复制到空html文件里点开就行。

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>popover教程实例</title>
</head>
<body>
    <button id="notifBtn" popovertarget="notifPop" popovertargetaction="toggle">查看消息</button>

    <div id="notifPop" popover>
        <h3>新通知</h3>
        <p>你的商品已通过审核。</p>
        <button type="button" id="markRead">我知道了</button>
    </div>

    <script>
        const popover = document.getElementById('notifPop');
        document.getElementById('markRead').addEventListener('click', function() {
            popover.hidePopover();
        });
    </script>
</body>
</html>

这里必须注意,按钮上面的popovertarget不一定非要用id引用,也可用class吗?不能,只能是目标元素的id。所以先给popdiv设一个id。

和之前的代码一样,如果点了按钮再点“我知道了”,会把弹层关掉。并且在弹层外点击,也会自动关掉。原生关闭动画比较平淡,过渡很平滑,不需要额外加transition。不过你可以在CSS里给popover加上动画效果,这里不扩展。

结语:理性看待新玩具

原生popover不能完全替代所有弹层库,但它确实把“轻量提示浮层”这个场景变得特别轻松。不用加载UI框架,也能编码出可用的交互,这种快乐很像原生CSS变量刚普及时期。

至少以后再有人问我怎么弹出一个气泡,我会先甩给他一个popover,再解释什么遮罩z-index。

既然是标准API,没有别人说得那么玄乎,还是自己动手去试最靠谱。希望这篇随笔能帮你少踩一点坑。

HTML popover属性:不用写一行JavaScript就能做弹层菜单
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html HTML popover属性:不用写一行JavaScript就能做弹层菜单 https://www.taomawang.com/web/html/2720.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务