本文深入解析如何借助元素与inert属性,打造符合 WCAG 2.1 A 级无障碍标准的模态交互组件,详细阐述浏览器原生 Tab 循环机制、焦点捕获原理以及无 JavaScript 实现下的边界条件。
在开发无障碍的模态交互组件(如汉堡菜单抽屉、弹窗提示)时,许多人误以为“Tab 键循环回到第一个可聚焦元素”必须通过 JavaScript 手动监听 keydown 事件并调用 focus() 来实现。然而,真正的模态行为并不需要手动循环逻辑——它是浏览器原生承诺的无障碍契约,而非开发者需要自行“修补”的功能。
你或许会问,这是否意味着可以完全摒弃手动焦点管理?关键在于,首先要理解什么是真正的模态。
✅ 正确前提:模态并非可见抽屉,而是语义化的焦点隔离
当用户点击汉堡图标展开导航抽屉时,若该抽屉被正确实现为模态上下文(modal context),浏览器将自动接管焦点流。具体而言:
- 调用
dialog.showModal()后,浏览器会自动启用焦点陷阱(Focus Trap)。即,当 Tab 键位于抽屉内最后一个可聚焦元素时,按下 Tab 会自然跳转到第一个可聚焦元素——注意,这不是立即跳转,而是在“离开”时循环; - 同时,背景内容将被完全禁用:所有外部元素不可聚焦、不可点击,屏幕阅读器也无法读取;
- 按下 ESC 键、点击背景遮罩(backdrop)或调用
close()方法均可安全退出,焦点会自动恢复到触发按钮。
这才是 的核心价值:它并非一个视觉容器,而是一个语义化的运行时上下文隔离器。这正是很多人容易忽略的要点——以为模态仅仅是“弹出一个框”,实际上更关键的是“焦点被限制在其中”。
⚠️ 注意:
showModal()是核心。如果仅使用show()或 CSSdisplay: block,则无法构成模态——此时 Tab 键仍可逃逸,必须依赖 JavaScript 实现焦点陷阱。
❌ 无 JavaScript 的“伪模态”无法真正实现 Tab 循环
如果坚持不使用 JavaScript(例如纯 CSS 抽屉),可以通过 :target 或 details/summary 实现展开效果,但无法满足 WCAG 对模态的要求。具体而言:
:target方案中,URL 哈希变化不会影响焦点管理,Tab 键仍可离开抽屉;inert属性虽然可以禁用外部交互(例如),但它不会接管焦点流,浏览器不会自动循环 Tab 键;- 此时所谓的“循环”实际上只是开发者手动通过 focus() 模拟的行为,而且很容易因事件时序问题(如误用 keyup)导致“刚抵达就跳走”的反模式体验。
/* 错误示范:inert 不能替代 showModal() */
main[inert] {
pointer-events: none;
opacity: 0.3;
}
/* ❌ 这不会让 Tab 在抽屉内循环 —— 仅视觉/交互禁用,焦点仍自由逃逸 */
✅ 最佳实践:分层实现,按需选择方案
| 场景 | 推荐方案 | 是否需 JS | 是否满足 WCAG A 级 |
|---|---|---|---|
| 现代浏览器 + 标准模态交互 | + showModal() |
✅(仅需控制开关) | ✅ 原生支持 |
| 需兼容 IE / Safari 低于 15.4 | 手动焦点陷阱(keydown + focus()) | ✅(必须) | ✅(需严格实现) |
| 纯展示型抽屉(非阻断式) | details + summary | ❌ | ❌(非模态,不适用) |
关键结论十分明确:“Tab 自动循环”并非一项功能,而是模态状态的必然结果。 你无需编写循环代码——你需要确保组件被正确声明为模态(role="dialog" + aria-modal="true" + 焦点捕获),或者直接使用 。任何试图在非模态上下文中“模拟循环”的做法,本质上都是在对抗浏览器内置的无障碍基础设施,最终只会导致维护成本激增和兼容性问题。
最后,无论采用何种方案,务必验证以下三点:
- 打开后,第一个可聚焦元素应自动获得焦点;
- Tab 键在内部元素间线性流动,到达末尾后自动回到起点;
- 关闭后,焦点回归触发源(如汉堡按钮),且背景内容不可聚焦。
这才是真正面向所有用户、可持续的模态交互设计。说起来,做好这件事其实比想象中简单得多——关键在于理解底层原理,而非盲目堆砌代码。
