模态框必须接管焦点并限制tab范围,关闭后恢复焦点至触发元素,设aria-modal="true"和role="dialog"并动态控制背景aria-hidden,esc和遮罩点击需保持焦点流,标题须用真实heading元素关联aria-labelledby。

模态框必须接管焦点且限制 Tab 键范围
用户按 Tab 时,焦点若逃出模态框,视障用户会彻底迷失上下文。这不是“建议”,而是 WCAG 2.1 A 级强制要求。
实操要点:
- 打开模态框后,立即将焦点用
focus()移到第一个可聚焦元素(如确认按钮或首个input) - 监听
keydown事件,捕获Tab键:当焦点在最后一个可聚焦元素上再按Tab,手动跳回第一个;反之亦然 - 所有可聚焦元素必须显式声明
tabindex="0"或使用原生可聚焦标签(button、a[href]、input等),避免依赖隐式 tabindex - 模态框关闭后,必须将焦点恢复到触发它的元素(比如点击的
button),否则键盘用户无法继续页面操作
aria-modal="true" 不是万能解,需配合 aria-hidden
aria-modal="true" 在主流浏览器中仍存在兼容问题——Safari 和旧版 Firefox 会忽略它,导致屏幕阅读器仍能读取背景内容。
更可靠的做法是双保险:
- 模态框根元素设
aria-modal="true"和role="dialog" - 同时给 所有 背景内容容器(通常是
或一个 wrapperdiv)动态添加aria-hidden="true" - 关闭模态框时,务必移除
aria-hidden="true",否则后续内容无法被读取 - 注意:不要对
直接设aria-hidden,某些屏幕阅读器会因此跳过整个页面;推荐用一个包裹全部主内容的<div id="app"> 来控制 <h3>ESC 键和点击遮罩层必须可取消,且不破坏焦点流</h3> <p>用户习惯按 <code>ESC关闭模态框,但若关闭后焦点丢失,等于把键盘用户“锁”在空白页里。关键细节:
-
ESC触发关闭时,必须同步执行焦点还原(见第一个副标题) - 遮罩层(backdrop)需设
tabindex="-1"并监听click,否则仅靠视觉点击无法满足键盘可访问性 - 遮罩层
click关闭逻辑里,要检查事件目标是否为遮罩层本身(e.target === backdropEl),而非其子元素,避免误关 - 禁止用
pointer-events: none隐藏遮罩交互——它会让屏幕阅读器完全忽略该区域,失去“点击外部关闭”的语义
标题必须用 heading 元素且与 role="dialog" 关联
仅靠
aria-label或aria-labelledby不足以让所有屏幕阅读器正确宣布模态框类型和目的。JAWS 和 NVDA 在某些模式下会跳过纯 aria 属性。稳妥写法:
- 模态框内第一个子元素必须是真实 heading(
<h2></h2>或更高阶),例如<h2 id="modal-title">删除文件</h2> - 根元素
dialog同时设aria-labelledby="modal-title" - 避免用
aria-label替代 heading——它无法提供层级语义,用户无法通过 heading 导航快速定位模态框 - 如果标题是动态渲染的(如含变量),确保 DOM 更新后
aria-labelledby指向的元素始终存在且文本已就绪
最常被忽略的一点:没有 heading 的模态框,在 VoiceOver + Safari 组合下,可能被直接宣布为“group”而非“dialog”,用户根本意识不到自己进了弹窗。
-











