无障碍焦点管理:弹窗打开以后,键盘用户不能迷路
前端无障碍里,焦点管理经常被忽略。弹窗打开后,键盘焦点还停在背景按钮;关闭弹窗后,焦点不知道回到哪里;抽屉里按 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 role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-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
Shift+Tab: cycle backward
Escape: close when allowed
Close: restore focus
Background: inert or unavailable
这份键盘契约应写进组件测试,而不是只靠人工试用。
五、总结
无障碍焦点管理要保证弹窗打开后焦点进入、Tab 不逃出、关闭后焦点恢复。配合正确的 role="dialog"、aria-modal 和可访问标题,组件才真正可操作。
好看的浮层不等于好用的浮层。键盘用户不迷路,才说明交互边界被认真设计过。
焦点管理做得好,鼠标用户通常不会察觉;但对键盘和读屏用户,它就是能不能完成任务的分界线。


被折叠的 条评论
为什么被折叠?



