← 中文资源

简体中文版本

使用 dialog 元素构建无障碍模态框

通过 showModal 打开原生模态框,关联标题与说明,支持 Esc、背景点击和取消优先焦点,并实时播报结果。

阅读英文原文

功能说明

页面提供一个删除项目的危险操作按钮,点击后用 dialog.showModal 打开确认框。对话框标题与说明分别通过 aria-labelledby 和 aria-describedby 关联,内部表单使用 method=“dialog”,取消与确认按钮的 value 会写入 returnValue。关闭事件根据返回值更新 role=“status” 的提示,并把焦点交还给触发按钮。Esc 与点击背景同样按取消处理。

关键代码

原生 showModal 会把对话框放入顶层,使页面其余内容不可交互,并自动限制焦点在对话框内,无需手写焦点陷阱。取消按钮带 autofocus,避免用户误按回车执行删除。表单提交时浏览器自动关闭对话框并设置 returnValue,因此不需要额外的提交处理。每次打开前把 returnValue 清空,避免上一次的 confirm 残留导致按 Esc 时被误判为已删除。关闭后显式调用 openButton.focus(),确保焦点回到触发位置。点击事件通过 event.target === dialog 判断是否点在背景或内边距上。

使用注意

原生 dialog 的焦点管理依赖浏览器实现,显式归还焦点可以覆盖兼容性差异。点击背景关闭对误触友好,但破坏性操作也可以选择禁止背景关闭。不要把对话框内容直接写在 form 外再依赖表单提交,returnValue 的语义会变得不清晰。对话框打开时不能嵌套另一个模态框。较旧浏览器可能不支持 dialog,需要评估目标环境或提供降级页面。

验证方式

用键盘 Tab 到删除按钮并按回车,确认焦点落在取消按钮,Tab 只能在对话框内循环。按 Esc 应关闭并显示删除已取消;再次打开后点击确认,显示项目已删除。点击背景空白处应关闭。关闭后检查焦点回到打开按钮,屏幕阅读器应读出对话框标题和说明。

代码示例

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Accessible confirmation dialog</title>
  <style>
    :root {
      color-scheme: light dark;
      font-family: system-ui, sans-serif;
    }

    body {
      margin: 0;
      padding: 2rem;
    }

    button {
      font: inherit;
      padding: 0.6rem 1rem;
      border: 1px solid currentColor;
      border-radius: 0.4rem;
      background: canvas;
      color: canvastext;
      cursor: pointer;
    }

    button:focus-visible {
      outline: 3px solid #4f46e5;
      outline-offset: 2px;
    }

    dialog {
      max-width: 32rem;
      border: 1px solid gray;
      border-radius: 0.75rem;
      padding: 1.5rem;
    }

    dialog::backdrop {
      background: rgb(0 0 0 / 55%);
    }

    .actions {
      display: flex;
      gap: 0.75rem;
      justify-content: flex-end;
      margin-top: 1.5rem;
    }
  </style>
</head>
<body>
  <main>
    <h1>Delete project</h1>
    <p>Deleting a project removes its deployments and environment variables.</p>
    <button type="button" id="open-dialog">Delete project…</button>
    <p id="status" role="status" aria-live="polite"></p>
  </main>

  <dialog id="confirm-dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
    <form method="dialog">
      <h2 id="dialog-title">Delete “Atlas”?</h2>
      <p id="dialog-desc">
        This permanently removes the project and its 12 deployments. This action cannot be undone.
      </p>
      <div class="actions">
        <button value="cancel" autofocus>Cancel</button>
        <button value="confirm">Delete permanently</button>
      </div>
    </form>
  </dialog>

  <script>
    const dialog = document.querySelector('#confirm-dialog');
    const openButton = document.querySelector('#open-dialog');
    const status = document.querySelector('#status');

    openButton.addEventListener('click', () => {
      dialog.returnValue = '';
      dialog.showModal();
    });

    dialog.addEventListener('close', () => {
      status.textContent = dialog.returnValue === 'confirm'
        ? 'Project deleted.'
        : 'Deletion cancelled.';
      openButton.focus();
    });

    dialog.addEventListener('click', (event) => {
      if (event.target === dialog) {
        dialog.close('cancel');
      }
    });
  </script>
</body>
</html>