概念

焦点陷阱

在模态对话框打开期间将键盘焦点限制在其内部的模式——防止 Tab 键逃出至底层文档,并在对话框关闭时将焦点还原至触发元素。

焦点陷阱是对键盘导航的一种有意限制:模态对话框打开时,Tab 键只在对话框内部的可聚焦元素之间循环;Shift+Tab 则反向循环。用户在未明确关闭对话框的情况下,无法通过 Tab 键返回模态框背后(视觉上被遮挡的)页面内容。

无障碍模态对话框必须实现焦点陷阱。若无焦点陷阱,键盘用户和屏幕阅读器用户将落在不可见的页面内容上,既看不到也无从逃脱。

为何必须如此

模态对话框在视觉上呈现为叠加在页面上的一层。其设计意图非常明确:用户先完成对话框中的操作,再返回页面。对于鼠标用户而言,这是直觉行为:点击模态框外部不起作用(或关闭对话框),点击内部则触达对话框的控件。

若没有焦点陷阱,键盘用户的体验将前后不一致:

  1. 模态框打开,焦点移至(或应移至)对话框内第一个可聚焦元素。
  2. 用户按 Tab,焦点移至对话框内下一个可聚焦元素。
  3. 用户持续按 Tab,焦点最终到达对话框内最后一个可聚焦元素。
  4. 再按一次 Tab。 若无陷阱,焦点将跳至文档中的下一个可聚焦元素——它位于模态框背后,用户不可见。

用户唯一的感知信号是模态框的焦点指示器消失了。他们无从得知焦点身在何处。

完整的模态框模式

一个完整的无障碍模态框应包含:

  1. 焦点移入对话框:打开时焦点移至第一个可聚焦元素,或移至对话框标题(若标题设有 tabindex="-1" 以支持程序化聚焦)。
  2. 焦点被限制在对话框内:从最后一个元素 Tab 可回绕至第一个;从第一个 Shift+Tab 可回绕至最后一个。
  3. Escape 键关闭对话框,并将焦点还原至打开对话框的触发元素(而非文档正文)。
  4. 点击对话框外部可关闭对话框——这是可选行为,并非所有模态框都支持;但若支持,在打开期间仍必须维持焦点陷阱。
  5. 在模态框元素上设置 role="dialog"aria-modal="true",并通过 aria-labelledby 指向模态框标题。

实现方案

最常被引用的库是 focus-trap(npm 包:focus-trapfocus-trap-reactfocus-trap-vue)——体积小巧、无额外依赖,可处理所有边界情况(跳过不可见元素、与 inert 集成、关闭时还原焦点)。

现代浏览器原生支持的方案是配合 dialog.showModal() 使用 <dialog> 元素,它可自动实现焦点陷阱并处理 Escape 键。目前浏览器兼容性已足够稳健,可作为基线方案。

反模式:“不释放的陷阱”

焦点陷阱必须在模态框关闭时解除。最常见的实现错误是忘记将焦点还原至原始触发元素——焦点随即落在文档正文上,屏幕阅读器对此无任何播报。用户在寂静中被抛下,不得不从页面顶部重新开始 Tab 导航。