document.activeElement 返回 null 的常见原因包括页面刚加载、所有元素失去焦点、或焦点落在不可聚焦节点(如普通 div)或 shadow root 外部;它只反映当前可聚焦且实际获得焦点的元素。
页面刚加载时、所有元素都失去焦点、或焦点落在 <body> 或 shadow root 外部时,document.activeElement 会返回 null 或 document.body。这不是 bug,而是浏览器标准行为——它只反映「当前有焦点的可聚焦元素」,不包括不可聚焦节点(如普通 <div>)或被 tabindex="-1" 主动排除的元素。
实操建议:
document.activeElement 调用 .focus() 或读取 .aria-label,先做存在性判断:const el = document.activeElement;<br>if (el && el !== document.body && el !== document.documentElement) {<br> // 安全操作<br>}
focusin 事件比轮询更可靠,尤其在单页应用中,路由切换后焦点可能未及时更新shadowRoot 边界:默认情况下 document.activeElement 不会穿透到 open shadow root 内部,得用 shadowRoot.activeElement
React 函数组件内直接读 document.activeElement 没问题,但若放进 useEffect 且依赖了某个 ref 或 state,容易因渲染时机导致读到旧值或触发不必要的重运行。
实操建议:
useRef 缓存上一次焦点元素,仅在 focusin 事件中更新,避免每次渲染都查 DOM:const lastFocusedRef = useRef(document.activeElement);<br>useEffect(() => {<br> const handleFocusIn = (e) => {<br> lastFocusedRef.current = e.target;<br> };<br> document.addEventListener('focusin', handleFocusIn);<br> return () => document.removeEventListener('focusin', handleFocusIn);<br>}, []);
document.activeElement 放进 useEffect 依赖数组——它不是响应式值,且频繁变化会导致无限循环document 不存在,必须加 typeof document !== 'undefined' 守卫focus 和 blur 事件不冒泡,而 focusin 和 focusout 会。这意味着你可以在 document 或某个容器上统一监听,无需为每个可聚焦元素单独绑定。
实操建议:
focusin 监听最外层容器即可捕获所有内部焦点变化focusin 在目标元素获得焦点前触发,适合做焦点拦截(比如阻止焦点离开弹窗);focusout 在焦点离开前触发,适合清理状态focusin 的兼容性:iOS 15.4+ 才完整支持,旧版需 fallback 到捕获阶段的 focus 事件document.activeElement 可能返回一个 input 或 button,但它未必对屏幕阅读器有效——比如缺少 aria-label、被 aria-hidden="true" 包裹、或父级有 inert 属性。
实操建议:
el.matches(':focusable')(非标准但现代浏览器支持)或手动检查:el.tabIndex >= 0 || el.tagName === 'A' || el.tagName === 'BUTTON' 等window.getComputedStyle(el).visibility !== 'hidden' && window.getComputedStyle(el).display !== 'none' 排除视觉隐藏但 DOM 仍在的元素el.getAttribute('aria-hidden') !== 'true' 且其任意父级也不含 aria-hidden="true",否则 NVDA/JAWS 会跳过它真实项目里,焦点管理最难的不是获取元素,而是确认它此刻是否「对辅助技术可见且可操作」——这需要组合 DOM 状态、样式、ARIA 属性三者判断,漏掉任一环都可能导致屏幕阅读器用户迷失。