本文详解如何通过 HTML 原生语义与现代 API(<dialog>、inert、aria-modal)构建符合 WCAG 2.1 A 级标准的模态菜单焦点循环,无需手动监听 keydown 实现“Tab 到末尾自动跳回首项”,避免常见 JS 焦点陷阱误用。
本文详解如何通过 html 原生语义与现代 api(``、`inert`、`aria-modal`)构建符合 wcag 2.1 a 级标准的模态菜单焦点循环,无需手动监听 keydown 实现“tab 到末尾自动跳回首项”,避免常见 js 焦点陷阱误用。
在构建响应式汉堡菜单(hamburger drawer)等模态导航组件时,一个关键无障碍需求是:当键盘用户按 Tab 键遍历菜单项到达最后一项后,焦点应自然循环回第一项——而非逃逸至页面其他区域,导致视障用户“迷失上下文”。许多开发者误以为必须用 JavaScript 监听 keydown 事件并手动 focus() 跳转,实则这是对模态语义理解的偏差:真正的焦点循环,本质是浏览器在正确模态上下文中自动提供的行为,而非需手写的逻辑。
浏览器原生支持焦点循环的前提,是组件被明确标识为模态(modal)上下文。这可通过以下任一方式实现:
<button id="toggleDrawer">☰ 菜单</button><dialog id="drawerMenu"> <h2>导航菜单</h2> <ul> <li><a href="/home">首页</a></li> <li><a href="/about">关于</a></li> <li><a href="/contact">联系</a></li> </ul> <button id="closeDrawer" aria-label="关闭菜单">✕</button></dialog>
const drawer = document.getElementById('drawerMenu');const toggleBtn = document.getElementById('toggleDrawer');const closeBtn = document.getElementById('closeDrawer');toggleBtn.addEventListener('click', () => drawer.showModal()); // 自动捕获焦点、禁用背景、支持 Esc/backdrop 关闭closeBtn.addEventListener('click', () => drawer.close());
✅ 优势:
若因样式或兼容性需自定义结构(如 div 实现 drawer),请严格遵循模态语义:
<!-- 触发按钮 --><button id="openDrawer">☰ 打开菜单</button><!-- 模态层结构 --><div id="drawerOverlay" class="overlay" hidden> <nav id="drawerNav" role="dialog" aria-labelledby="drawerTitle"> <h2 id="drawerTitle">主菜单</h2> <ul> <li><a href="/products">产品</a></li> <li><a href="/pricing">价格</a></li> <li><a href="/support">支持</a></li> </ul> <button id="closeDrawerManual">关闭</button> </nav></div><!-- 主内容区(模态激活时设 inert) --><main id="mainContent"> <h1>网站首页</h1> <p>……</p></main>
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; // ✅ 关键:禁用主内容交互与可访问性暴露 // 浏览器会自动将焦点移入 nav 内首个可聚焦元素(如第一个 <a>)});closeBtn.addEventListener('click', () => { overlay.hidden = true; mainContent.inert = false; openBtn.focus(); // ✅ 关闭后恢复焦点至触发按钮});
⚠️ 注意:inert 是现代标准(Chrome 105+、Firefox 119+、Safari 16.4+),旧浏览器需 polyfill(如 wicg-inert)。若无法使用 inert,则必须手动添加 aria-hidden="true" 并移除所有 tabindex,但 inert 更可靠、更语义化。
| 要素 | 正确实践 | 错误实践 |
|---|---|---|
| 容器语义 | <dialog> 或 role="dialog" + aria-labelledby | 仅用 div 无任何 ARIA 标注 |
| 背景禁用 | showModal() 或 inert | 仅靠 opacity: 0.2 或 z-index 遮挡 |
| 焦点管理 | 依赖浏览器自动捕获/恢复 | 手动 focus() + keydown 监听 |
| 关闭后焦点 | 显式调用 triggerButton.focus() | 忽略,焦点停留在 body 或丢失 |
截至 2026 年,主流浏览器已全面支持 <dialog> 与 inert,建议新项目默认采用 <dialog> 实现模态菜单;存量项目可渐进升级,用 inert 替代手工 aria-hidden + tabindex 控制。记住:无障碍不是“附加功能”,而是通过正确语义让浏览器替你完成合规性保障。