autofocus在异步场景下无效,须用requestanimationframe+focus()并校验activeelement;动态内容需重算焦点陷阱集合,确保可访问性属性完备且视觉隐藏不破坏语义。

autofocus在异步刷新后完全无效,别白费力气
HTML autofocus 属性只在初始 HTML 解析阶段起作用,对 AJAX 加载、v-if 切换、React useState 触发的 DOM 更新等异步场景毫无反应。你看到它写在模板里却没聚焦,不是代码写错了,是浏览器根本没机会处理它。
常见错误包括:给动态插入的 <input> 加 autofocus 属性、用 el.setAttribute('autofocus', '') 试图“激活”、或依赖 SSR 渲染时带上的该属性——hydration 不会重触发解析逻辑。
- 同一页面多个
autofocus,浏览器通常只响应第一个,其余静默丢弃 - SPA 路由跳转后,
autofocus不会自动重生效 - iOS Safari 在非用户手势上下文(如 iframe、后台标签)中直接禁用
autofocus
focus() 必须配合 requestAnimationFrame 才可靠
异步内容插入后立刻调 input.focus(),大概率失败——DOM 节点虽已存在,但浏览器尚未完成样式计算或布局,元素可能被 display: none 或 visibility: hidden 隐藏,此时聚焦会被静默忽略。
用 setTimeout(() => input.focus(), 0) 也不够稳,它只保证在下一个宏任务执行,不保证渲染帧就绪。真正该用的是:
requestAnimationFrame(() => {
if (input && document.activeElement !== input) {
input.focus({ preventScroll: true });
}
});
-
requestAnimationFrame确保聚焦发生在浏览器下一次绘制前,此时元素已可视且可聚焦 - 加
preventScroll: true避免 iOS Safari 自动滚动导致界面跳动(Safari 15.4+ 支持) - 务必检查
document.activeElement !== input,防止打断用户正在输入
焦点目标必须满足可访问性前提
即使 focus() 调用成功,屏幕阅读器(如 TalkBack、NVDA)也可能不播报,因为语义缺失或时机不对。这不是 JS 问题,而是可访问性链路断了。
- 确保目标元素有明确可访问名称:
aria-label、aria-labelledby或原生<label for="id"></label> - 若聚焦容器(如弹窗根节点),必须先设
tabindex="-1",否则无法获得焦点 - 非表单元素(如
<div>)仅靠 <code>tabindex="0"可聚焦,但部分旧版 TalkBack 对其focus()不主动播报,需搭配aria-live="polite"区域触发通知 - 避免用
display: none或visibility: hidden控制显隐——改用position: absolute; clip: rect(0 0 0 0);实现视觉隐藏但保留可访问性
动态内容更新后必须重算焦点陷阱范围
模态框或表单通过 fetch 插入新字段后,原先缓存的可聚焦元素列表就失效了。继续用旧数组做 Tab 循环判断,会导致焦点逃逸到背景页,破坏键盘导航流。
每次内容变更后,必须重新收集:
const focusables = container.querySelectorAll('button, input, select, textarea, [tabindex="0"]');
- 不要缓存
first/last变量,应在keydown回调内实时获取当前focusables - 使用
tabbable库时,必须显式调用trap.update(),否则新增<input>不参与循环 - 监听
focusin事件(而非只监听keydown),才能捕获鼠标点击、屏幕阅读器跳转等所有失焦路径
最易被忽略的点:焦点陷阱不是“写一次就完事”,而是每次 DOM 变更后都要重置可聚焦集合——哪怕只是追加一个 <button></button>,也得重新查一遍。











