popover 与 dialog
区分 `` 与 `popover` 的适用场景:会阻断流程、需先处理的用 ``,不阻断、点外部可关闭的用 `popover`,并介绍用原生 HTML、CSS 和少量 JS 实现模态确认框、下拉菜单和提示气泡。
学完这篇,你能分清 <dialog> 和 popover 各自该用在什么场景,并用原生 HTML + CSS + 少量 JS 手写出模态确认框、下拉菜单和提示气泡,不再为这类组件引入整个 UI 库。
第一步:先分清两者解决的不是同一类问题
这一步要建立判断标准,做完之后你看到一个需求就能立刻决定用哪个元素。
<dialog> 是对话框:它会打断用户当前流程,强迫用户先处理它。典型场景是「确认删除吗」「填写这个表单」,用户不点掉它就没法继续操作页面。
popover 是弹出层:它不打断流程,点页面其他地方就自动消失。典型场景是「更多操作」菜单、日期选择面板、气泡提示、分享面板。
一句话判断法:用户不处理它,流程能不能继续? 不能,用 <dialog>;能,用 popover。
| 维度 | <dialog> + showModal() | popover |
|---|---|---|
| 是否阻断 | 阻断,背景不可点 | 不阻断 |
| 点击外部 | 不关闭 | 默认关闭 |
| 焦点 | 自动锁在框内 | 不移出焦点 |
| 背景遮罩 | ::backdrop | 无 |
| Esc 键 | 关闭 | 关闭(auto 模式) |
第二步:写出第一个模态对话框
这一步要做出一个能弹出、带遮罩、按 Esc 能关的确认框,做完你就掌握了 <dialog> 最核心的用法。
HTML 结构:
<dialog id="confirmDlg">
<form method="dialog">
<h2>确认删除?</h2>
<p>删除后无法恢复。</p>
<menu>
<button value="cancel">取消</button>
<button value="ok">删除</button>
</menu>
</form>
</dialog>
<dialog> 元素默认是 display: none,不调用方法就不会显示。必须用 JS 打开:
const dlg = document.getElementById('confirmDlg');
dlg.showModal(); // 模态打开,有遮罩、有焦点陷阱
注意这里是 showModal(),不是 show()。两者的区别很大:
showModal():模态。显示::backdrop遮罩,焦点被限制在对话框内,Esc 可关闭,dialog::backdrop样式生效。show():非模态。没有遮罩,焦点也不会被限制,只是浮在上面而已,用的人很少。
form method="dialog" 是个关键技巧:表单里的 <button value="xxx"> 被点击后,对话框自动关闭,并把 value 写进 dlg.returnValue。所以你不需要自己写关闭逻辑。
注意:
<dialog>必须在文档流中(document.body内),不要放在<template>里再动态插入,否则showModal()前需要先appendChild,容易忘记。
第三步:拿到用户的选择并处理返回值
这一步要把「用户点了哪个按钮」读出来,做完你的对话框才真正有业务价值。
dlg.addEventListener('close', () => {
if (dlg.returnValue === 'ok') {
// 执行删除
}
});
除了表单方式,也可以直接调用 dlg.close('someValue') 主动关闭并传值。想取消 Esc 关闭,监听 cancel 事件并 preventDefault():
dlg.addEventListener('cancel', (e) => {
e.preventDefault(); // 禁止 Esc 关闭,强制用户做选择
});
遮罩样式改 ::backdrop 伪元素:
dialog::backdrop {
background: rgb(0 0 0 / 0.4);
backdrop-filter: blur(2px);
}
注意:
::backdrop只对showModal()打开的对话框生效,show()打开时不会出现,这是新手最常见的困惑。
第四步:写出第一个 popover 下拉菜单
这一步要做出一个点击按钮弹出、点别处自动收起、不需要写一行 JS 的菜单。
<button popovertarget="menu" popovertargetaction="toggle">更多操作 ▾</button>
<div id="menu" popover>
<ul>
<li><button>重命名</button></li>
<li><button>复制链接</button></li>
<li><button>删除</button></li>
</ul>
</div>
只要写了 popover 属性(等价于 popover="auto"),再给触发按钮加上 popovertarget="目标id",浏览器就自动完成打开、关闭、Esc 关闭、点击外部关闭。popovertargetaction 可选 toggle(默认)、show、hide。
JS 侧对应三个方法:
menu.showPopover();
menu.hidePopover();
menu.togglePopover();
以及两个事件:beforetoggle(可 preventDefault() 阻止)和 toggle,事件对象里的 newState / oldState 是 "open" 或 "closed"。
注意:
popover元素默认自带display: none,它的显示状态由浏览器通过:popover-open伪类控制。如果你写#menu { display: flex; },会直接覆盖掉默认的display: none,导致菜单一直显示在页面上。正确写法是#menu:popover-open { display: flex; }。
第五步:选对 popover 的三种模式
这一步要搞清 auto / manual / hint 的区别,做完你就能正确处理「多层菜单」这类复杂情况。
popover="auto"(默认):有 light dismiss(点外部关闭),且打开一个新的 auto popover 时,会自动关闭其他 auto popover。popover="manual":点外部不关、Esc 不关,完全靠 JS 控制。适合需要一直停留的面板,比如「正在上传」的浮层。popover="hint":有 light dismiss,但不会关闭其他 auto popover。这是给 tooltip 这类辅助提示准备的(Chrome 133 起支持)。
多级菜单用 hint 才不会一打开子菜单就把父菜单关掉。
注意:
hint是三者中最新的一个,老版本浏览器会把它当成无效值并回退成auto。如果你的 tooltip 依赖它来保持父菜单打开,需要做特性判断。
第六步:用 CSS 锚点定位让弹出层贴在触发元素旁
这一步要解决「弹出层位置乱跑」的问题,做完你的菜单能自动对齐按钮,还会在空间不够时自动翻转到反方向。
以前大家都用 Popper.js 算坐标,现在用 CSS Anchor Positioning(Chrome 125+):
#trigger { anchor-name: --menu-anchor; }
#menu {
position: absolute;
position-anchor: --menu-anchor;
position-area: bottom span-right; /* 挂在下方、横向对齐 */
margin-top: 4px;
position-try-fallbacks: flip-block, flip-inline; /* 放不下就翻面 */
}
position-area 用一个九宫格式的值描述弹层放在锚点的哪个方向,position-try-fallbacks 负责空间不足时的降级方案。
配一套入场动画,dialog 和 popover 写法一样:
#menu {
opacity: 0;
transform: translateY(-4px);
transition: opacity .15s, transform .15s,
display .15s allow-discrete, overlay .15s allow-discrete;
}
#menu:popover-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
#menu:popover-open {
opacity: 0;
transform: translateY(-4px);
}
}
注意:
allow-discrete和@starting-style缺一不可。只写transition是没用的,因为display是离散属性,不加allow-discrete浏览器不会对它做过渡;不加@starting-style,浏览器找不到「起始状态」,动画也不会播放。
第七步:查兼容性并做降级
这一步要确认你的目标浏览器是否支持,做完你就知道要不要引入 polyfill。
<dialog> 支持面已经很广:Chrome 37+、Firefox 98+、Safari 15.4+。
popover 相对新:Chrome 114+、Firefox 125+、Safari 17+。可以用一行代码做特性检测:
const hasPopover = HTMLElement.prototype.hasOwnProperty('popover');
if (!hasPopover) {
// 引入 @oddbird/popover-polyfill,或退回自己写 JS 控制
}
CSS 锚点定位更激进(Chrome 125+、Safari 26+),建议这样写降级:先给弹层一个 position: fixed; top: ...; left: ... 的兜底定位,再用 @supports (anchor-name: --x) { ... } 覆盖。
注意:
<dialog>和popover都自带无障碍语义(role、焦点管理、键盘行为)。如果你用<div>自己模拟,这些全都要手动补 ARIA 属性,工作量比你想的大得多。能用原生就用原生。
小结
<dialog>用来阻断流程,popover用来做非阻断的轻量浮层,判断标准是「用户不处理它能不能继续」。
原文链接:https://www.gj0.com/thread-1676.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。