无障碍焦点管理:弹窗打开以后,键盘用户不能迷路
无障碍焦点管理弹窗打开以后键盘用户不能迷路前端无障碍里焦点管理经常被忽略。弹窗打开后键盘焦点还停在背景按钮关闭弹窗后焦点不知道回到哪里抽屉里按 Tab 跑到页面底部。这些问题不会影响鼠标用户却会让键盘用户和读屏用户迷路。焦点管理不是锦上添花而是交互组件的基本功能。尤其是弹窗、抽屉、菜单、浮层必须认真处理。一、弹窗焦点要形成闭环flowchart TD A[Trigger Button] -- B[Open Dialog] B -- C[Move Focus Into Dialog] C -- D[Trap Focus] D -- E[Close Dialog] E -- F[Restore Focus To Trigger]打开时焦点进入弹窗弹窗内 Tab 循环关闭后回到触发按钮。这个闭环缺一环键盘用户都会失去上下文。二、使用语义属性弹窗应该有正确的 role、标题和模态属性。div roledialog aria-modaltrue aria-labelledbydialog-title h2 iddialog-title删除项目/h2 button取消/button button确认删除/button /div如果没有可访问标题读屏用户只听到“对话框”不知道它在处理什么任务。三、实现焦点陷阱可以使用成熟库也可以自己实现基本逻辑。核心是找到弹窗内可聚焦元素并在首尾循环。const focusable dialog.querySelectorAll( button, [href], input, select, textarea, [tabindex]:not([tabindex-1]) ); const first focusable[0]; const last focusable[focusable.length - 1];自己实现时要考虑动态内容、disabled 元素和嵌套浮层。复杂组件建议使用经过验证的无障碍基础库。四、关闭后恢复焦点弹窗打开前保存触发元素关闭后恢复。const trigger document.activeElement; openDialog(); function closeDialog() { dialog.remove(); trigger?.focus?.(); }如果触发元素已经不存在比如删除了当前列表项就应该把焦点移动到合理的替代位置例如列表容器或下一个操作按钮。还要处理 Escape 和背景滚动。模态弹窗打开时Escape 通常应关闭弹窗页面背景不应继续滚动。关闭动作也要和焦点恢复配合不能先移除节点再丢失引用。dialog_keyboard_contract: Tab: cycle inside dialog ShiftTab: cycle backward Escape: close when allowed Close: restore focus Background: inert or unavailable这份键盘契约应写进组件测试而不是只靠人工试用。五、总结无障碍焦点管理要保证弹窗打开后焦点进入、Tab 不逃出、关闭后焦点恢复。配合正确的roledialog、aria-modal和可访问标题组件才真正可操作。好看的浮层不等于好用的浮层。键盘用户不迷路才说明交互边界被认真设计过。焦点管理做得好鼠标用户通常不会察觉但对键盘和读屏用户它就是能不能完成任务的分界线。