键盘用户依赖Tab键在可聚焦元素之间移动焦点,如果页面样式过度重置或自定义组件缺少焦点反馈,用户很容易失去操作位置。使用jQuery检测用户是否正在通过键盘导航,本质上是记录最近一次输入方式,并在文档根元素上维护一个状态类,再由CSS决定是否显示焦点轮廓。这个做法可以让键盘用户获得更明显的焦点提示,同时避免鼠标用户每次点击按钮时都看到多余轮廓。

为什么需要检测键盘导航而不是直接显示焦点
无障碍访问并不只是让屏幕阅读器能读出内容,还要保证键盘用户可以完成操作。Tab键负责把焦点从链接移到按钮,再移到表单控件,如果焦点轮廓被隐藏,用户就不知道当前焦点在哪里。像 <button> 这样的原生元素默认有焦点样式,但很多项目会统一写成 outline: none,再自定义样式,一旦忘记补充焦点态,就会造成键盘用户迷失。
直接给所有 :focus 状态显示强轮廓也不是理想方案。鼠标点击也会触发焦点,如果用户只是点击一个按钮,页面上却出现粗重的蓝色轮廓,视觉体验会显得突兀。CSS中的 :focus-visible 已经能缓解这个问题,但它在部分旧浏览器或复杂自定义组件中支持不稳定,尤其是动态插入的弹窗、下拉菜单、标签页组件,仍然需要一层可控的状态判断。
用jQuery检测键盘导航,可以把最近一次输入方式保存在页面状态中。当检测到键盘操作时,在 html 根节点添加 keyboard-nav 类,CSS就可以只在这个类存在时显示焦点环。这个状态还能用于显示跳过导航链接、展开菜单、高亮当前焦点项等无障碍交互,不需要把逻辑写死在每一个组件内部。
用jQuery记录输入方式并同步焦点状态
核心思路很简单,键盘操作时记录为keyboard,鼠标或触摸操作时记录为pointer,然后在焦点进入元素时根据输入方式决定是否添加根节点类。这里不建议只监听Tab键,因为键盘用户还可能使用方向键、回车键、空格键进入组件,所以可以把常见导航键一起作为键盘信号。反过来,只要用户使用了鼠标或触摸,就认为指针输入更可能影响当前交互。
(function ($) {
var $root = $('html');
var inputMode = 'pointer';
function setKeyboardMode(active) {
if (active) {
$root.addClass('keyboard-nav');
} else {
$root.removeClass('keyboard-nav');
}
}
$(document).on('keydown', function (e) {
if (e.key === 'Tab' || e.which === 9) {
inputMode = 'keyboard';
setKeyboardMode(true);
}
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
inputMode = 'keyboard';
setKeyboardMode(true);
}
});
$(document).on('mousedown touchstart', function () {
inputMode = 'pointer';
setKeyboardMode(false);
});
$(document).on('focusin', function () {
if (inputMode === 'keyboard') {
setKeyboardMode(true);
} else {
setKeyboardMode(false);
}
});
})(jQuery);
这段代码使用事件委托,不需要在每个元素上单独绑定。 keydown 负责识别键盘操作, mousedown 和 touchstart 负责识别指针操作, focusin 则在焦点真正落到某个元素时同步根节点状态。这样做的好处是,即使用户先点击链接,再用Tab继续导航,下一次键盘操作也会重新打开键盘状态,不会一直停留在pointer。
样式部分可以保持简单。只要根节点有 keyboard-nav,就让可聚焦元素显示清晰轮廓。为了避免轮廓紧贴边框,可以设置 outline-offset,让焦点环和控件之间留出一点距离。如果页面本身已经使用了 :focus-visible,也可以把它作为增强,而不是唯一依赖。
html.keyboard-nav :focus {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
html.keyboard-nav :focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
处理弹窗动态内容和反向Tab导航
普通表单控件的焦点顺序通常由DOM顺序决定,但弹窗、抽屉、菜单等组件会改变键盘用户的预期。一个可访问的弹窗打开后,焦点应该移动到弹窗内部,关闭时焦点应该回到触发按钮。如果弹窗内存在多个可聚焦元素,还需要限制Tab键在弹窗内部循环,避免键盘用户跳到背景内容里。jQuery在这里适合处理焦点移动和Tab边界,但语义结构仍然要交给原生元素或合适的ARIA角色。
function openDialog($dialog, $trigger) {
var $root = $('html');
$dialog.attr('aria-hidden', 'false').show();
$dialog.data('trigger', $trigger);
$root.addClass('keyboard-nav');
$dialog.find('button, a, input, textarea, select').first().focus();
}
function closeDialog($dialog) {
var $trigger = $dialog.data('trigger');
$dialog.attr('aria-hidden', 'true').hide();
if ($trigger) {
$trigger.focus();
}
}
function trapTabFocus($dialog, e) {
var $focusables = $dialog.find('a[href], button:not(:disabled), input:not(:disabled), textarea:not(:disabled), select:not(:disabled), [tabindex]:not([tabindex="-1"])').filter(':visible');
var $first = $focusables.first();
var $last = $focusables.last();
if (e.key === 'Tab' || e.which === 9) {
if (e.shiftKey && document.activeElement === $first[0]) {
e.preventDefault();
$last.focus();
} else if (!e.shiftKey && document.activeElement === $last[0]) {
e.preventDefault();
$first.focus();
}
}
}
$(document).on('keydown', '.dialog', function (e) {
trapTabFocus($(this), e);
});
上面的 trapTabFocus 会收集弹窗内可见且可聚焦的元素,当焦点位于第一个元素并按下Shift加Tab时,把焦点送到最后一个元素;当焦点位于最后一个元素并按下Tab时,把焦点送回第一个元素。选择器中的 [tabindex="-1"] 用来排除那些只是程序化聚焦、不希望参与键盘循环的元素。实际项目里,弹窗内容可能异步加载,所以最好在渲染完成后重新计算可聚焦元素,而不是在页面初始化时一次性绑定。
对于单页应用,路由切换后旧焦点可能失效,新页面内容又需要键盘用户从头开始。可以在路由完成钩子里把焦点移到主内容区域,例如给一个 <main> 元素添加 tabindex="-1" 并调用 focus()。这样键盘用户不会停留在上一个页面已经移除的按钮上。类似地,下拉菜单打开时,焦点可以进入菜单第一项;关闭时,焦点回到触发菜单的按钮。
常见误区和可落地的验收方法
第一个误区是只监听Tab键。键盘用户进入自定义组件后,可能使用方向键移动选项,使用回车键或空格键确认,使用Esc键关闭浮层。如果只把Tab当作键盘导航信号,状态就会不稳定。第二个误区是依赖 e.keyCode,这个属性已经被废弃,虽然jQuery事件里仍可能提供 e.which,但优先使用 e.key,再为老环境保留简单兜底。第三个误区是点击后不清除键盘状态,导致鼠标用户也看到持续存在的焦点轮廓。
验收时不要只看鼠标操作,应该关闭鼠标,只使用键盘完成核心流程。比如登录页要能依次聚焦用户名输入框、密码输入框、记住密码复选框、登录按钮,并能通过回车提交。弹窗要能打开、循环、关闭,并回到触发元素。页面顶部最好有一个跳过导航链接,平时可以隐藏,键盘Tab到它时再显示出来。像 <a> 这样的跳过链接不要只靠颜色区分,也要有足够对比度和清晰文本。
无障碍优化不是给页面打补丁,而是让焦点状态、输入状态和组件行为保持一致。jQuery检测键盘导航的价值在于把用户输入方式变成可管理的状态,让CSS和交互逻辑都能基于这个状态做增强。只要实现时注意反向Tab、动态内容、弹窗焦点返回和语义化结构,就能让键盘用户和鼠标用户都获得更稳定的体验。