:focus-visible在鼠标点击后不触发不是bug,而是浏览器ua启发式判断逻辑所致:若首次交互为鼠标,chrome/firefox会进入“指针优先”模式,后续tab导航可能暂不激活该伪类,需用户主动按tab或esc才恢复;验证应直接用devtools手动勾选:focus-visible测试样式是否生效。

为什么:focus-visible在鼠标点击后有时不触发
这不是 bug,是浏览器的 UA 启发式判断逻辑在起作用:如果页面首次交互是鼠标(比如加载完立刻点按钮),Chrome/Firefox 会进入“指针优先”模式,后续 Tab 导航可能暂时不激活 :focus-visible,直到用户主动按 Tab 或 Esc —— 这属于正常行为,不是样式没写对。
验证方法很简单:打开 DevTools → Elements 面板 → 选中目标元素 → 点击右上角 :hov 按钮 → 手动勾选 :focus-visible,看样式是否生效。如果能显示,说明 CSS 没问题,只是当前交互上下文没满足触发条件。
- 别依赖“点完再按 Tab”来测试:鼠标点击后立即按 Tab,部分浏览器仍判定为延续指针会话,
:focus-visible不触发 - 真正可靠的测试路径是:页面加载 → 直接按
Tab(不碰鼠标)→ 观察第一个可聚焦元素 - 移动端 Safari 在软键盘弹出前,
input获得焦点也不会触发:focus-visible,这是已知限制,不是 Polyfill 能绕过的
如何让自定义组件(如)支持:focus-visible
加 tabindex="0" 不够,浏览器会拒绝给无语义或无交互能力的元素启用 :focus-visible 判断逻辑。必须同时满足三个条件:
- 有明确角色:
role="button"、role="link" 或其他可交互 ARIA role
- 监听键盘事件:
onkeydown 中处理 Enter、Space,否则浏览器认为“这个元素不响应键盘”,直接跳过 :focus-visible 计算
- 确保元素可获得焦点:不能被
pointer-events: none 或 visibility: hidden 阻断
例如,一个封装的开关组件,只写 <div tabindex="0" role="switch"> 是不够的;必须加上 <code>onkeydown={handleKeydown},且 handleKeydown 至少能响应 Enter,否则 Chrome 89+ 会静默忽略该元素的 :focus-visible 匹配。
移动端触摸设备下:focus-visible基本不可用,怎么兜底
Safari iOS 和 Android Chrome 对 :focus-visible 的支持非常有限:触摸 tap 后焦点几乎从不触发该伪类,且没有可靠方式通过 JS 模拟(因为 focus 事件本身在非用户手势上下文中被屏蔽)。这时候必须放弃“精准区分”,改用更保守的策略:
- 对
input、textarea 等原生表单控件,保留 :focus 样式,但用 outline-offset 和高对比色保证可访问性
- 对按钮类元素,移动端统一用
:active + :focus 组合样式,例如:button:focus, button:active { background-color: #e6f7ff; }
- 避免在移动端 CSS 中单独依赖
:focus-visible 做关键视觉反馈,它在那里大概率是“不存在”的
换句话说:移动端不谈 :focus-visible,只谈 :focus 是否足够清晰、是否干扰点击反馈。
旧版 Chrome(
js-focus-visible 是目前唯一靠谱方案,但它不是“开了就自动好”,容易在以下环节出错:
- 引入脚本后,CSS 必须改用类选择器:
button.js-focus-visible .focus-visible,不能继续写 button:focus-visible,否则旧版浏览器照样忽略整条规则
- 必须补降级规则:
button:focus:not(.js-focus-visible .focus-visible) { outline: none; },否则旧版 Chrome 会回退到默认 outline,而你又没覆盖它
- Webpack/Vite 项目里,如果用了
import 'js-focus-visible',务必在 package.json 中设 "sideEffects": false,否则 tree-shaking 可能删掉初始化逻辑,脚本加载了但没运行
最常被忽略的一点是:Polyfill 不会自动为 iframe 内容或 Shadow DOM 中的元素打标,这部分需单独初始化,否则子应用/组件库里的按钮依然没 focus-visible 效果。
加 tabindex="0" 不够,浏览器会拒绝给无语义或无交互能力的元素启用 :focus-visible 判断逻辑。必须同时满足三个条件:
- 有明确角色:
role="button"、role="link"或其他可交互 ARIA role - 监听键盘事件:
onkeydown中处理Enter、Space,否则浏览器认为“这个元素不响应键盘”,直接跳过:focus-visible计算 - 确保元素可获得焦点:不能被
pointer-events: none或visibility: hidden阻断
例如,一个封装的开关组件,只写 <div tabindex="0" role="switch"> 是不够的;必须加上 <code>onkeydown={handleKeydown},且 handleKeydown 至少能响应 Enter,否则 Chrome 89+ 会静默忽略该元素的 :focus-visible 匹配。
移动端触摸设备下:focus-visible基本不可用,怎么兜底
Safari iOS 和 Android Chrome 对 :focus-visible 的支持非常有限:触摸 tap 后焦点几乎从不触发该伪类,且没有可靠方式通过 JS 模拟(因为 focus 事件本身在非用户手势上下文中被屏蔽)。这时候必须放弃“精准区分”,改用更保守的策略:
- 对
input、textarea等原生表单控件,保留:focus样式,但用outline-offset和高对比色保证可访问性 - 对按钮类元素,移动端统一用
:active+:focus组合样式,例如:button:focus, button:active { background-color: #e6f7ff; } - 避免在移动端 CSS 中单独依赖
:focus-visible做关键视觉反馈,它在那里大概率是“不存在”的
换句话说:移动端不谈 :focus-visible,只谈 :focus 是否足够清晰、是否干扰点击反馈。
旧版 Chrome(
js-focus-visible 是目前唯一靠谱方案,但它不是“开了就自动好”,容易在以下环节出错:
- 引入脚本后,CSS 必须改用类选择器:
button.js-focus-visible .focus-visible,不能继续写button:focus-visible,否则旧版浏览器照样忽略整条规则 - 必须补降级规则:
button:focus:not(.js-focus-visible .focus-visible) { outline: none; },否则旧版 Chrome 会回退到默认outline,而你又没覆盖它 - Webpack/Vite 项目里,如果用了
import 'js-focus-visible',务必在package.json中设"sideEffects": false,否则 tree-shaking 可能删掉初始化逻辑,脚本加载了但没运行
最常被忽略的一点是:Polyfill 不会自动为 iframe 内容或 Shadow DOM 中的元素打标,这部分需单独初始化,否则子应用/组件库里的按钮依然没 focus-visible 效果。











