Skip to content
返回设计中心

模态与抽屉的焦点陷阱

约束 Tab 循环在覆盖内并在关闭时恢复焦点。

无障碍进阶

为什么

无焦点陷阱,模态内 Tab 逃到背后页 — 键盘用户交互不可见内容。陷阱焦点使他们留在模态直到显式关闭。

WebAIM Million 2024 报告发现 96.3% 的首页存在可检测的 WCAG 失败。2023 年 ADA 相关诉讼超过 4,500 起,使无障碍成为法律和伦理必需,而非可有可无。

何时使用

每模态、对话框、抽屉、覆页弹出用焦点陷阱。内联折叠跳过 — 它们是页流一部分。

如何做

1. 开时 — 存 document.activeElement 作恢复目标,移焦点到模态首个可聚焦 2. 陷阱 — Tab 时若在末可聚焦且按 Tab,循环到首;首处 Shift+Tab 循环到末 3. 恢复 — 关时对存的目标调 .focus()(开模态的按钮) 4. 库 — focus-trap-react(3KB)处理边界;或 Radix Dialog / Headless UI Dialog 5. 初始焦点 — 聚焦模态标题或首输入,非容器(容器需 tabindex=-1) 6. 守卫 — 忽模态外点击(或外点击关),但绝不让焦点离 7. 嵌套 — 第二陷阱堆叠;关内恢复到外,非页

示例

import { FocusTrap } from "focus-trap-react"; function Modal({ open, onClose, title, children }: Props) { const restoreRef = useRef<HTMLElement | null>(null); useEffect(() => { if (!open) return; restoreRef.current = document.activeElement as HTMLElement; return () => restoreRef.current?.focus(); // 关时恢复 }, [open]); if (!open) return null; return ( <FocusTrap> <div role="dialog" aria-modal="true" aria-labelledby="t"> <h2 id="t" tabIndex={-1}>{title}</h2> {children} <button onClick={onClose}>关闭</button> </div> </FocusTrap> ); } // 或手卷:查可聚焦,在边界处理 Tab/Shift+Tab const focusables = (el: HTMLElement) => el.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');

常见问题

  • ⚠无焦点恢复 — 模态关,焦点落 body,屏幕阅读器用户迷
  • ⚠陷阱漏动态加可聚焦 — 表单字段渲染后 Tab 逃
  • ⚠初始焦点在容器 — 屏幕阅读器播报 div 非对话框目的

最佳实践

  • ✓关时始终恢复焦点 — 回到开模态的按钮,非 document.body
  • ✓用维护库(focus-trap-react、Radix Dialog)— 处理动态可聚焦 + 嵌套陷阱
  • ✓开时聚焦标题或首输入 — 给屏幕阅读器用户即时上下文

常见问题解答