模态框必须手动管理焦点以满足wcag 2.1 aa合规:打开时用requestanimationframe聚焦首个可交互元素,tab键需拦截循环聚焦,关闭后须恢复至触发源或主内容区。

模态框打开后焦点没落到第一个可交互元素上
浏览器不会自动把焦点移到弹窗里,哪怕你用 dialog.showModal() 或 display: block 显示了遮罩层。用户按 Tab 键依然从页面顶部开始遍历,对屏幕阅读器用户来说,弹窗内容等于“不可达”。
必须手动聚焦。常见错误是等 DOM 渲染完就立刻调 focus(),但若弹窗含异步加载内容(比如 fetch 后插入表单),此时目标元素可能还没挂载。
- 优先选弹窗内第一个
tabindex="0"、button、input、select或带语义的可聚焦元素 - 用
requestAnimationFrame()包一层再聚焦,比setTimeout(, 0)更可靠 - 如果弹窗由第三方库(如 Bootstrap Modal)控制,查它是否暴露
shown.bs.modal这类事件,别自己硬等
示例:
const modal = document.getElementById('my-modal');
modal.showModal();
requestAnimationFrame(() => {
const firstFocusable = modal.querySelector('button, input, select, [tabindex="0"]');
if (firstFocusable) firstFocusable.focus();
});
Tab 键在模态框内不循环,而是直接跳出到背景
默认 Tab 会一路走到页面底部,穿出弹窗。必须拦截并循环聚焦——但不能简单监听 keydown 拦所有 Tab,否则会破坏屏幕阅读器的内置导航逻辑(比如 NVDA 的 Insert+F7 列表)。
正确做法是只在焦点即将离开弹窗时干预,且仅限键盘 Tab/Shift+Tab。重点看 event.key 和当前聚焦元素是否为弹窗内最后一个/第一个可聚焦项。
- 用
modal.querySelectorAll('button, input, select, [tabindex="0"]')预先缓存焦点元素列表 - 监听弹窗容器的
keydown,仅当event.key === 'Tab'且当前聚焦的是列表末尾/开头时,event.preventDefault()并调用focus() - 不要用
element.matches(':focusable')判断可聚焦性(Safari 不支持),改用getComputedStyle(element).visibility !== 'hidden' && getComputedStyle(element).display !== 'none'辅助过滤
用 <dialog></dialog> 就不用管焦点捕获了?
要。虽然 <dialog></dialog> 在 Chrome/Firefox 中调用 showModal() 后会禁用背景交互,但 Tab 键依然能离开模态框(尤其当内部无聚焦元素或含 tabindex="-1" 容器时)。Safari 更是完全不处理焦点限制。
所以无论是否用 <dialog></dialog>,只要要求 WCAG 2.1 AA 合规,就必须手写 trap focus 逻辑。
-
<dialog></dialog>默认不渲染,光写标签、加class、设style都没用,必须调showModal() - 别用
dialog.hidden = true/false或dialog.style.display控制显隐——这会绕过原生焦点和 backdrop 逻辑 -
aria-modal="true"是给辅助技术用的语义声明,**完全不影响原生 Tab 行为**
关闭模态框后焦点丢失,无法回到触发源
关闭弹窗时若不手动恢复焦点,焦点会落到 document.body 或随机元素上,下一次 Tab 就断链了。尤其当弹窗是从按钮触发时,用户期望 Esc 后焦点回到那个按钮。
关键不是“记下哪个元素打开了弹窗”,而是“在弹窗关闭前存好它”。容易踩的坑是:把触发元素存在闭包里,但该元素后来被移除或重渲染,导致 focus() 失败且无报错。
- 用
data-trigger-id这类属性把触发源 ID 存到弹窗 DOM 上,关闭时再查 - 调用
element.focus()前加个if (element.offsetParent !== null)判断是否还在文档流中 - 避免依赖
document.activeElement——弹窗关闭瞬间它可能是body,不是你想要的来源 - 关闭前缓存触发元素,检查它是否仍可聚焦;否则退回到主内容区(如
main或h1)
disabled、aria-hidden 或 CSS visibility: hidden 拦截;以及 Safari 对 focus() 的静默忽略——它要求用户至少触屏过一次页面,才允许 JS 主动聚焦非原生可聚焦元素。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











