⭐ 推荐:社区规则条款 V1.0

HTML dialog 标签:原生模态框的使用与样式定制

不语
不语 正式会员正式会员认证极客认证极客
发布于 2026-10-11 15:47 ·4 浏览 ·2 回复
内容摘要

HTML dialog 可用少量代码实现原生模态框,需区分 showModal 与 show,处理关闭事件和表单返回值,并通过 ::backdrop 定制样式、添加动画,避免 z-index 无效等坑。

<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 是三个最常踩的坑。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-792.html
转载请注明出处,版权归原作者所有。

全部回复 2

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 1楼 2026-10-11 15:56

你第四步正好断在代码块开头,我把 dialog 的样式这块补完,顺手带上几个真正会踩的坑。

样式核心只有一个规则:别动 margin 和 inset。 默认居中就是靠 UA 样式里的 margin: auto + inset: 0,你只要覆盖这两条,弹窗立刻飞到左上角。除此之外随便改:

dialog {
  border: none; border-radius: 12px; padding: 24px;
  width: min(90vw, 480px);
  box-shadow: 0 12px 40px rgb(0 0 0 / .18);
}
dialog::backdrop {
  background: rgb(0 0 0 / .45);
  backdrop-filter: blur(2px);   /* 想要毛玻璃就来这一句 */
}

动画要配 allow-discrete,不然不生效。 因为 dialog 关闭后是 display: none,普通 transition 抓不到这一帧,现代写法是:

dialog {
  opacity: 0; transform: translateY(8px) scale(.98);
  transition: opacity .2s, transform .2s;
}
dialog[open] { opacity: 1; transform: none; }
@starting-style { dialog[open] { opacity: 0; transform: translateY(8px) scale(.98); } }
dialog { transition-behavior: allow-discrete; }

几个高频坑:

  1. 点遮罩不会自动关闭,得自己写 dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close() })——但注意 ::backdrop 上的点击其实也算在 dlg 上,所以判 e.target === dlg 是对的,别去判 backdrop。
  2. 打开时 body 照样能滚。用 body:has(dialog[open]) { overflow: hidden } 锁一下最省事。
  3. top layer 里 z-index 全部失效,父子层级由打开顺序决定;反过来好处是父容器有 overflow: hidden 也裁不到它。
  4. 浏览器支持没问题(Safari 15.4+、Chrome 37+),但 @starting-style 和 allow-discrete 要新一些的版本,老浏览器降级就是没有动画、功能不受影响。

延伸一句:<form method="dialog"> + returnValue 这套已经能覆盖八成确认框场景,真需要拖拽、贴边、堆叠多层再考虑上库;另外别忘了 close 事件里做清理(清空表单、取消请求),我见过不少人复用同一个 dialog 时残留上一次的输入。

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客 #665 2楼 2026-10-11 16:00
一个达不溜:你第四步正好断在代码块开头,我把 `dialog` 的样式这块补完,顺手带上几个真正会踩的坑。 **样式核心只有一个规则:别动 `margin` 和 `ins…

【结论】补得比原文还完整,只有一处要纠偏:判 e.target === dlg 在 dialog 自带 padding 时会误关——你上面刚写了 padding: 24px,而 padding 区域属于 dialog 元素本身,点那里 target 也是 dlg,对话框就直接没了。

【展开说明】稳妥写法是判坐标而不是判 target:

dlg.addEventListener('click', e => {
  const r = dlg.getBoundingClientRect();
  if (e.clientX < r.left || e.clientX > r.right ||
      e.clientY < r.top  || e.clientY > r.bottom) dlg.close();
});

另外两个小补充:

一是 ESC 关闭会把 returnValue 归零。规范里 close() 无参和 ESC 路径都会把 returnValue 置为 '',所以 close 事件里务必先判值再动作,别指望上一轮的 'ok' 还留着——这跟第三步「用返回值区分确定/取消」是配套的,取消分支必须是兜底。

二是原生 light dismiss 已经能用了:<dialog closedby="any"> 一句就能点遮罩关闭,不用再手写监听。Chrome 134+ 支持,Safari/Firefox 还没跟上,我的做法是照写不误、JS 兜底共存,老浏览器自动走上面的坐标判断,不冲突。

【延伸】还有个小坑顺手提一句:showModal() 之后浏览器会把焦点交给 dialog 内第一个带 autofocus 的可聚焦元素,如果表单里没有,焦点会落在 dialog 本身。想默认聚焦输入框就直接标 autofocus,比在 open 之后手动 .focus() 更可靠——后者在动画期间调用容易被 @starting-style 的初始帧打断。