focus() 失败主因是元素不可聚焦或 DOM 未就绪;需设 tabindex="0"、用 requestAnimationFrame 延迟调用、检查 display:none;模态框须手动实现焦点围栏并精准拦截 Tab 循环;优先使用原生语义化标签而非 ARIA。

focus() 失败时先查 tabindex 和渲染时机
调用 element.focus() 没反应,90% 是因为元素不可聚焦或 DOM 还没就绪。原生 button、input、a[href] 默认可聚焦;div、span 必须加 tabindex="0" 才能进 Tab 流。但别用 tabindex="-1" 后再调 focus() —— 它只支持程序聚焦,不参与键盘导航。
动态插入的元素(比如 fetch 后 append 的表单字段)不能立即 focus()。实操建议:
- 用
requestAnimationFrame(() => el.focus()),确保样式和布局已计算完成 - 避免
setTimeout(() => ..., 0),它不保证渲染完成,尤其在 Shadow DOM 场景下更不可靠 - 检查元素是否被
display: none隐藏 —— 此时focus()直接静默失败
模态框必须手动实现焦点围栏,showModal() 不够用
<dialog> 的 showModal() 确实设了 aria-modal="true" 并响应 Esc,但在 Safari 全版本和旧 Edge 中,它不触发真正的焦点围栏:Tab 键仍能跳出对话框,document.activeElement 可能停留在 body 上。
必须补三件事:
立即学习“前端免费学习笔记(深入)”;
- 打开后立刻聚焦首个可聚焦项:
modal.querySelector('button, input, select, [tabindex="0"]') - 监听
keydown拦截 Tab/Shift+Tab,在焦点到末尾/开头时event.preventDefault()并跳转到对端 - 关闭前缓存触发源(推荐存
data-trigger-id),关闭后校验该元素是否仍在 DOM 中(el.offsetParent !== null),再.focus()
Tab 导航循环要精准拦截,不是所有 Tab 都该拦
全局监听 keydown 并 preventDefault() 所有 Tab 键,会破坏屏幕阅读器的内置导航(如 NVDA 的 Insert+F7 列表)。正确做法是只在焦点即将越界时干预。
关键步骤:
- 预缓存可聚焦元素列表:
const focusables = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') - 在
modal上监听keydown,仅当event.key === 'Tab'且当前document.activeElement是列表首/末项时才拦截 - 用
focusables[0].focus()或focusables[focusables.length - 1].focus()实现循环,而非硬编码索引
语义化标签比 ARIA 更优先,别用 div + role="button" 替代 button
原生 button 自带焦点管理、空格/回车触发、disabled 状态语义和屏幕阅读器识别;而 div 加 role="button" 只是“告诉辅助技术它像按钮”,但不提供任何交互逻辑 —— 你得手写键盘事件、状态反馈、禁用处理,稍有遗漏就不可访问。
真正需要 ARIA 的场景有限:
-
aria-label用于无文本图标按钮(如<button aria-label="删除">×</button>) -
aria-labelledby关联标题与区域(如<dialog aria-labelledby="title"><h2 id="title">设置</h2>) -
inert属性可禁用整块区域,但需 polyfill 降级(Safari 不支持)
最常被忽略的点:焦点样式必须用 :focus-visible 而非 :focus,否则鼠标用户会看到冗余轮廓线;而 autofocus 在模态框中慎用 —— 它可能覆盖掉用户原本的焦点位置,破坏导航连续性。



















