aria-modal="true"单独使用无效,仅是语义标记,无法限制tab导航;必须配合role="dialog"、手动焦点围栏(捕获tab键、循环跳转、精准恢复触发源焦点)及动态aria-hidden控制背景。

模态框不加手动焦点管理,aria-modal="true"基本等于没设——Safari 和旧版 Firefox 会直接忽略它,键盘用户按 Tab 仍会溜进页脚或 header,屏幕阅读器可能读取背景内容甚至卡死。
为什么 aria-modal="true 单独用不行
这个属性只是告诉辅助技术“这是个模态上下文”,不触发原生焦点围栏。实测中:document.activeElement常停留在 body,焦点根本没进模态框;NVDA + Firefox 组合下,aria-modal="true"完全不生效。
- 必须搭配
role="dialog",缺一不可;role缺失时,部分读屏器连“对话框”语义都不识别 - Safari 17.4 之前不支持
<dialog></dialog>的showModal()焦点围栏,得 fallback 到 JS 实现 - 别指望浏览器自动聚焦:打开后不调
firstFocusableElement.focus(),键盘用户第一 Tab 就会跳空
怎么写一个真正可用的焦点围栏
核心是捕获 Tab 键并手动循环跳转,不是靠属性“声明”就完事。
- 缓存可聚焦元素列表:
Array.from(modal.querySelectorAll('button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))—— 注意排除tabindex="-1" - 监听
keydown,当event.key === 'Tab'时:
– 焦点在最后一个元素上再按 Tab,event.preventDefault()并focus()第一个
– 按Shift+Tab到第一个时,focus()最后一个 - 首次打开后立刻
firstFocusableElement.focus(),不要等 CSS 动画结束(否则焦点位置错乱) - 关闭时必须恢复到触发源(如
id="open-modal-btn"),不是document.body.focus();用queueMicrotask(() => triggerBtn.focus())替代setTimeout,避免 Firefox 失效
背景隐藏不能只靠 aria-hidden="true"
aria-hidden="true" 是继承式屏蔽:父元素设了,子元素再写 aria-hidden="false" 也无效。更糟的是,对 直接加它,某些屏幕阅读器会跳过整个页面。
- 正确做法:给包裹主内容的容器(比如
<div id="app">)动态加 <code>aria-hidden="true",关闭时移除 - 现代方案优先用
inert属性(Chrome/Firefox/Edge 支持良好),<div id="app" inert> 能同时禁用焦点、点击和读屏访问 <li> <code>aria-modal="true"要和aria-hidden="true"双保险,不能只信其中一个 - 标题必须用真实 heading 元素(如
<h2 id="modal-title"></h2>),并通过aria-labelledby="modal-title"关联,不能用aria-label硬编码
最容易被忽略的点是:所有可聚焦元素必须显式参与 Tab 流——button、a[href] 这类原生可聚焦标签没问题,但自定义组件里用 div 模拟按钮,就得配 tabindex="0" 并监听 Enter/Space,否则键盘用户根本点不动。











