概览
对模态和非模态对话框使用原生 <dialog> 元素。它提供了无障碍语义、焦点处理、顶层渲染和 Esc 处理,而这些都是自定义叠加层必须手动重建的功能。
选择叠加层类型
选择第一个匹配的规则。如果不确定,请使用规则 1。
规则 1:叠加层会干扰用户并遮挡页面(默认)
用于确认、表单、登录、破坏性操作以及任何未另行说明的“模态”“对话框”“灯箱”或“弹出式窗口”请求。
使用 <dialog> 以模态形式打开,以声明方式使用 command="show-modal"(请参阅声明式对话框和弹出式窗口控件),或通过脚本使用 showModal()。
<button type="button" command="show-modal" commandfor="confirm">Delete</button>
<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this item?</h2>
<form method="dialog">
<button value="cancel" autofocus>Cancel</button>
<button value="delete">Delete</button>
</form>
</dialog>
浏览器提供:惰性页面、焦点限制、顶层、::backdrop、Esc 和返回手势关闭,以及焦点返回到调用方。
- 请勿添加
role="dialog"、aria-modal、焦点陷阱、inert切换或Esc处理程序。 - 请勿使用
show()或open属性打开该文件。 - 务必使用以下 CSS 停止页面在对话框后面滚动。如需了解详情,请参阅防止模态对话框下方的页面滚动。
- 请勿在
html或body上使用overflow: hidden或position: fixed锁定滚动。
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
overscroll-behavior: contain;
}
不适用于菜单或提示:请参阅规则 3。
规则 2:用户在面板打开时继续在页面中工作
用于查找栏、检查器、调色板和撰写窗格,这些在语义上仍属于对话框。
使用 <dialog popover> 可创建具有弹出式窗口关闭功能的顶层面板(auto 会轻触关闭,manual 不会)。仅在面板不得位于顶层时使用 dialog.show()。
<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
<h2 id="find-title">Find</h2>
<input type="search" aria-label="Search">
<button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>
该页面仍可互动,不会变为非活跃状态。
- 务必添加可见的关闭按钮。
- 请勿使用
showModal(),这会屏蔽网页。
不适用于屏蔽确认:请参阅规则 1。
规则 3:叠加层不是对话框
用于菜单、下拉菜单、提示、悬停卡片和消息框。
使用具有 popover 属性(而非 <dialog>)的语义元素。
<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
<li><a href="/rename">Rename</a></li>
</ul>
选择如何关闭
默认值取决于对话框的打开方式。如果 closedby 缺失或无效,showModal() 的行为将与 closerequest 相同,而任何其他打开方式的行为将与 none 相同。
- 仅限 Esc 键和返回手势(模态默认值):不设置任何内容,或设置为
closedby="closerequest"。 - 点击外部区域也可关闭:使用
closedby="any"。 - 仅由开发者关闭(例如,包含未保存更改的表单):使用
closedby="none"并添加确认路径。请参阅下文的“结语”。
指南
无障碍名称和初始焦点
- 务必为每个对话框命名,使用
aria-labelledby指向其标题,或者在没有可见标题时使用aria-label。 - 务必将
autofocus放在主要控件或第一个字段上。 - 务必在破坏性确认中将
autofocus放在安全操作(例如“取消”或“保留”)上。 - 请勿编写代码来在关闭时返回焦点。浏览器会将其返回给调用方。
开场白
- 要在
<button>上使用command="show-modal"和commandfor以声明方式打开。请参阅声明式对话框和弹出式窗口控件。 - 务必在通过脚本打开时使用
dialog.showModal()。
结束语
- 务必使用
<form method="dialog">按钮或command="close"关闭。 - 务必谨慎选择
closedby:any(轻触关闭和Esc)、closerequest(仅限Esc,模态默认值)或none(仅限开发者控制的关闭)。 - 务必对必须遵循“未保存更改”逻辑的“取消”按钮使用
command="request-close"或requestClose()。它们会先触发cancel,并忽略closedby。 - 务必将
closedby视为渐进增强。Esc和移动设备返回手势已经可以在没有该按钮的情况下关闭模态窗口。
挂接到关闭
- 务必在
close监听器中读取dialog.returnValue,以了解使用了哪个按钮,然后使用dialog.returnValue = ''将其重置。Esc和其他关闭请求不会设置任何值,并且浏览器在是否清除先前值方面存在差异,因此如果不进行重置,过时的值可能会泄露。将''视为“未选择即关闭”。 - 务必使用
cancel拦截Esc和request-close,但不要在没有用户激活的情况下依赖preventDefault()来处理重复的Esc按压。 - 务必将
closedby="none"与明确的确认步骤结合使用,以处理脏表单。 - 务必在
cancel被阻止时告知用户对话框保持打开状态的原因:显示一条可见消息,并使用aria-describedby从获得焦点的控件(例如“舍弃”)引用该消息,以便屏幕阅读器在焦点移动时朗读该消息。如果没有该属性,当焦点移至新按钮时,系统只会播报该按钮的名称。 - 请勿让确认操作将控件移到指针下方。保留消息的空间(使用
visibility隐藏,而不是display或hidden),并在现有按钮不会移动的位置(例如,右对齐行的第一个位置)添加破坏性按钮,这样在同一位置再次点击就不会舍弃用户的更改。
非模态对话框
- 务必对顶层面板使用
<dialog popover>(auto或manual),在不需要顶层时使用dialog.show()。 - 务必在每个非模态对话框中包含可见的关闭按钮。
背景幕和样式
- 务必设置
::backdrop的样式,并使用dialog:modal专门定位到模态样式。 - 建议将离开动画留给为元素添加进入和离开顶层的动画,将拖动留给构建可拖动元素,将重定父级留给永久性顶层界面。
- 请勿无条件地在
dialog上设置display。它会覆盖关闭状态。使用dialog[open]限定其范围。
无障碍
- 请勿向原生模态窗口添加
role="dialog"、aria-modal、手动焦点陷阱或inert切换。 - 务必使用具有无障碍名称的原生
<button>作为关闭控件。 - 务必为对话框标题使用一致的标题级别。
后备策略
closedby (dialog-closedby)
即使没有 closedby,模态窗口仍可通过 Esc 和返回手势关闭;只是失去了点击外部区域关闭的功能,因此这属于优雅降级。closedby="none" 会以同样的方式被忽略:Esc 会触发 cancel,然后关闭对话框。将未保存更改防护机制保留在 cancel 处理程序中,而不是仅保留在 closedby="none" 中,这样它也可以在缺少 closedby 的情况下提供保护。如果您的基准目标需要轻触关闭,请在缺少 closedby 的位置添加以下代码:
if (!('closedBy' in HTMLDialogElement.prototype)) {
// Test the pointer position, not event.target: the <dialog> is also the target for its own
// padding and the gaps between its children, so a target check would close on those clicks
const outside = (e, dialog) => {
const r = dialog.getBoundingClientRect();
return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
};
for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
let pressedOutside = false;
dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
// Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
}
}
overscroll-behavior
将滚动包含视为渐进增强。请勿进行特征检测:即使不可滚动容器会忽略该属性,@supports 也会报告该属性受支持。在未强制执行该属性的情况下,只要对话框自身的内容溢出,对话框仍会包含滚动条,并且 showModal() 仍会使页面处于非活动状态。请勿在 html 或 body 上回退到 overflow: hidden 或 position: fixed,这会移除根滚动条并改变布局。
Invoker 命令(command/commandfor)
如果您的基准目标不包含调用器命令,请保留相同的标记并有条件地加载 polyfill(与渐进增强相同的体验)。在接口上检测,绝不无条件加载:
// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill'); // npm install invokers-polyfill
}
只有规则 2 和 3 使用了 Popover。
Popover 的回退和浏览器支持
Popover API 主要是一种渐进增强技术,但其核心行为(顶层提升、轻触关闭和 popovertarget 调用)没有仅限 CSS 的等效项。旧版浏览器需要 polyfill,如果您不想提供 polyfill,则需要手动后备方案。
Polyfill:如需在旧版浏览器中支持 popover 属性,请有条件地加载 @oddbird/popover-polyfill。强制性要求:通过检查 HTMLElement.prototype 上的 popover 属性来检测功能,并且仅在缺少原生支持时加载 polyfill,请勿无条件加载。
使用打包工具或导入映射:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
如果没有打包器,请从 <script type="module"> 内的 CDN 导入:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
样式注意事项:polyfill 无法定义真正的 :popover-open 伪类,因此它会改为应用 .\:popover-open 类。强制性:使用 :is() 或 :where() 将两者结合,否则缺少 :popover-open 的浏览器会舍弃整个规则:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
或者,对于没有 polyfill 的旧版后备方案,请使用 position: fixed 并通过 getBoundingClientRect() 手动计算坐标,或依赖 inset: auto 的默认定位(如果该用例可以接受)。