<dialog> 是浏览器原生提供的模态框元素,学完这篇你能用十几行代码写出不输弹窗库的模态框,并且知道怎么改样式、加动画、避开那几个常见的坑。
第一步:最小可用示例
<button id="open">打开</button>
<dialog id="dlg">
<p>这是一个原生模态框</p>
<button id="close">关闭</button>
</dialog>
<script>
const dlg = document.getElementById('dlg');
document.getElementById('open').onclick = () => dlg.showModal();
document.getElementById('close').onclick = () => dlg.close();
</script>
不用引入任何库。showModal() 一调用,浏览器就替你做了三件事:把对话框提到 top layer(顶层渲染层)、把对话框以外的页面设为 inert(不可点击不可聚焦)、绑定 ESC 键关闭。
第二步:分清 show() 和 showModal()
showModal():模态。有遮罩、焦点锁在里面、按 ESC 关闭、页面其余部分不可交互。
show():非模态。就是个浮在上面的普通元素,没有遮罩,不锁焦点,ESC 也不管用。
两个方法都遵循「已打开时再调用会报错」的规则,所以打开前可以判一下 if (!dlg.open) dlg.showModal()。
关闭方式有三种:dlg.close()、ESC 键、以及 <form method="dialog"> 里的提交按钮。打开和关闭分别触发 close / cancel(ESC 会先触发 cancel,事件可以 preventDefault() 阻止关闭)事件,需要做清理逻辑时监听它们。
注意:HTML 里直接写 <dialog open> 等同于非模态的 show(),不会出现遮罩,别拿它当模态用。
第三步:用返回值区分「确定 / 取消」
在 dialog 内部放一个 method="dialog" 的表单,提交按钮的 value 会自动成为 dialog.returnValue:
<dialog id="dlg">
<form method="dialog">
<p>确定要删除吗?</p>
<button value="cancel">取消</button>
<button value="ok" autofocus>确定</button>
</form>
</dialog>
<script>
dlg.addEventListener('close', () => {
if (dlg.returnValue === 'ok') doDelete();
});
</script>
这样连关闭按钮的绑定都可以省掉,表单提交即关闭。
第四步:样式定制
dialog 的默认样式来自浏览器 UA 样式表,核心是 margin: auto + inset: 0,所以它才会居中。你自己写样式时不要把 margin 或 inset 改掉,否则会跑到左上角:
dialog {
padding: 24px;
border: none;
border-radius: 12px;
width: min(90vw, 420px);
box-shadow: 0 12px 40px rgb(0 0 0 / .2);
}
dialog::backdrop {
background: rgb(0 0 0 / .5);
backdrop-filter: blur(2px);
}
::backdrop 只在模态模式下存在,它不在 DOM 里,也不继承 dialog 的样式,只能单独写。想改遮罩颜色、加模糊,都在这里。
注意:模态 dialog 在 top layer,z-index 对它完全无效,别指望用 z-index 调整它和页面上其他元素的层级。
第五步:加进出场动画
进场直接写 open 状态的动画;出场麻烦一点,需要 @starting-style 和 allow-discrete 才生效:
dialog {
opacity: 0;
transform: translateY(8px);
transition: opacity .2s, transform .2s, overlay .2s allow-discrete, display .2s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: translateY(0);
}
@starting-style {
dialog[open] { opacity: 0; transform: translateY(8px); }
}
overlay 和 display 这两个属性的过渡,是让元素在动画播完之前不要从顶层被移除的关键,漏写就变成「一关就消失」。
第六步:这几个坑先知道
点遮罩关闭要自己实现。 原生不提供这个能力。常用做法是判断点击坐标是否落在 dialog 矩形之外:
dlg.addEventListener('click', e => {
const r = dlg.getBoundingClientRect();
const outside = e.clientX < r.left || e.clientX > r.right ||
e.clientY < r.top || e.clientY > r.bottom;
if (outside) dlg.close();
});
背景页面可能还能滚。 模态对话框会锁焦点,但部分浏览器仍允许背景滚动,可以补一句 body:has(dialog[open]) { overflow: hidden }。
dialog 里的按钮默认 type="submit"。 如果这个 dialog 恰好嵌套在外层 <form> 里,点击会提交外层表单。保险起见显式写 type="button"。
多开时后开的在上面。 top layer 按打开顺序堆叠,后开的盖住先开的,这个行为是确定的,不用额外处理。
兼容性。 Safari 15.4 起才支持 showModal(),IE 完全不支持。需要照顾老浏览器时得用 polyfill,或退回自己写的弹层方案。
小结
- 模态用
showModal(),非模态用 show(),两者行为差别很大。
- 关闭路径有三条:
close()、ESC、method="dialog" 表单提交,返回值走 returnValue。
- 改样式别动
margin: auto 和 inset: 0,遮罩样式写在 ::backdrop 里。
- 出场动画必须配
@starting-style 和 allow-discrete。
- 点遮罩关闭、背景滚动锁定、按钮
type 是三个最常踩的坑。