本文深入解析如何借助 HTML 原生语义及现代 API(、inert、aria-modal),打造符合 WCAG 2.1 A 级标准的模态菜单焦点循环机制。无需手动监听 keydown 事件,即可实现“Tab 键移至末尾后自动跳回首项”,有效规避常见的 JS 焦点陷阱误用问题。
在构建响应式汉堡菜单这类模态导航组件时,一项至关重要的无障碍需求是:当键盘用户按下 Tab 键遍历菜单项,焦点抵达最后一项后,应自然循环回第一项——而非逃逸至页面其他区域,进而避免视障用户“迷失操作上下文”。这里需要强调一个核心观念:真正的焦点循环,实质上是浏览器在正确的模态上下文中自动触发的原生行为,并非需要开发者手写维护的复杂逻辑。
换句话说,不少开发者的第一反应往往是:写一个 keydown 监听事件,手动跳转焦点。但这背后存在一个常见的认知偏差——你真正需要做的,仅仅是告知浏览器“这是一个模态组件”,剩余的工作,浏览器自身会妥善处理。
✅ 正确做法:借助语义化模态容器接管焦点流
浏览器原生支持焦点循环的前提,在于组件被明确标识为模态上下文。这可通过以下任一方式实现:
方案一:优先采用 元素
这是现代标准推荐的首选方案。代码示例如下:
const drawer = document.getElementById('drawerMenu');
const toggleBtn = document.getElementById('toggleDrawer');
const closeBtn = document.getElementById('closeDrawer');
toggleBtn.addEventListener('click', () => drawer.showModal());
closeBtn.addEventListener('click', () => drawer.close());
此方案的优势十分显著:showModal() 方法自动启用焦点陷阱,Tab 键在菜单内自然循环;同时自动设置 aria-modal="true"、管理 aria-hidden 状态、恢复触发元素焦点——无需手动配置 tabindex 或事件监听,浏览器原生即可保障 WCAG 合规性。
方案二:手动模态结构 + inert 属性
若因样式或兼容性需求需自定义结构(例如使用 div 实现抽屉菜单),则需严格遵循模态语义:
网站首页
……
const overlay = document.getElementById('drawerOverlay');
const mainContent = document.getElementById('mainContent');
const openBtn = document.getElementById('openDrawer');
const closeBtn = document.getElementById('closeDrawerManual');
openBtn.addEventListener('click', () => {
overlay.hidden = false;
mainContent.inert = true; // 关键:禁用主内容交互与可访问性暴露
});
closeBtn.addEventListener('click', () => {
overlay.hidden = true;
mainContent.inert = false;
openBtn.focus(); // 关闭后恢复焦点至触发按钮
});
⚠️ 注意:
inert是现代标准属性(Chrome 105+、Firefox 119+、Safari 16.4+),老旧浏览器需借助 polyfill。若无法使用 inert,则必须手动添加aria-hidden="true"并移除所有tabindex,但inert方案更可靠、语义化程度更高。
❌ 常见错误与风险警示
- 不要仅靠 CSS
visibility: hidden或display: none控制可访问性:这些属性仅影响视觉呈现,屏幕阅读器仍可读取内容,键盘仍可聚焦元素。 - 避免手动实现“Tab 循环”逻辑:例如监听 keyup 判断焦点位置再调用
focus()—— 这种做法极易引发“到达即跳转”的 UX 缺陷,用户刚 Tab 到最后一项就被强制跳走,体验大打折扣。更关键的是,这违背了浏览器原生的焦点管理机制。 - 非模态场景禁止焦点循环:若菜单设计为非阻断式,用户可同时操作菜单与页面,那么绝不能限制 Tab 流——否则相当于创建无法退出的焦点陷阱,违反 WCAG 2.1 2.4.3 与 2.1.2 准则。
总结:无障碍焦点循环 = 正确的模态语义 × 浏览器原生能力
| 要素 | 正确实践 | 错误实践 |
|---|---|---|
| 容器语义 | 或 role="dialog" + aria-labelledby |
仅用 div 无任何 ARIA 标注 |
| 背景禁用 | showModal() 或 inert |
仅靠 opacity: 0.2 或 z-index 遮挡 |
| 焦点管理 | 依赖浏览器自动捕获/恢复 | 手动 focus() + keydown 监听 |
| 关闭后焦点 | 显式调用 triggerButton.focus() |
忽略,焦点停留在 body 或丢失 |
截至 2026 年,主流浏览器已全面支持 与 inert。建议新项目默认采用 实现模态菜单;存量项目可渐进升级,用 inert 替代手工 aria-hidden + tabindex 控制。请牢记:无障碍并非“附加功能”,而是通过正确语义让浏览器替你完成合规性保障。
