:focus-visible 必须与 :focus 搭配使用且 :focus 在前,否则键盘用户有焦点反馈而鼠标用户无任何视觉提示;浏览器仅在 tab、方向键等键盘导航时触发它,鼠标点击、js focus() 均不触发;safari 15.3 及更早版本忽略该伪类,需 :focus 降级兜底。

只写 :focus-visible 样式,键盘用户能看见焦点,鼠标用户会彻底失去反馈——这不是 bug,是设计行为。必须搭配 :focus 一起用,且顺序不能错。
为什么 :focus-visible 单独用会失效
浏览器对 :focus-visible 的匹配有严格条件:它只响应 Tab、Shift+Tab、方向键、Enter、Space 这类键盘导航行为;鼠标点击、触摸 tap、甚至 JS 调用 element.focus() 都不会触发它。所以如果只写 button:focus-visible { outline: 2px solid #007aff; },鼠标点按钮后页面毫无视觉反馈,用户根本不知道操作是否生效。
更关键的是兼容性降级逻辑:Safari 15.3 及更早版本会直接忽略整条 :focus-visible 规则;如果没有前置的 :focus 声明,这部分用户就完全看不到任何焦点样式。
- ✅ 正确写法:
button:focus { outline: 1px dotted #999; }在前,button:focus-visible { outline: 2px solid #007aff; outline-offset: 2px; }在后 - ❌ 错误写法:
:focus-visible写在:focus前面,或两者都用!important导致层叠混乱 - ⚠️ 风险操作:全局写
*:focus { outline: none; }——这会把:focus-visible的样式也一并干掉
哪些元素默认不触发 :focus-visible?怎么补救
不是加了 tabindex="0" 就自动支持。浏览器会根据语义和交互意图判断是否启用该伪类。常见“假可聚焦”场景:
<div role="button"> 类组件:必须监听 <code>keydown事件,显式响应 Enter 和 Space,否则 Chromium 可能判定为“非交互意图”,拒绝激活:focus-visible- 自定义
contenteditable容器:它自身不触发:focus-visible,需手动加tabindex="0",并在focusin中通过 JS 切换.focus-visible类模拟 -
<select></select>用了appearance: none:部分浏览器会丢弃原生焦点样式,得手动给select:focus-visible补 outline - 禁用态(
aria-disabled="true"或disabled):即使有tabindex="0",也不会匹配:focus-visible——这是正确行为,别强行绕过 - 用 DevTools Elements 面板手动勾选目标元素的
:focus和:focus-visible状态,比反复切键盘更快验证匹配逻辑 - 运行
console.log(document.activeElement),确认焦点真落在目标元素上,且它的tabIndex ≥ 0、未被disabled、没有display: none - 检查是否在重置样式中对父级或通用选择器用了
outline: none,比如button:focus { outline: none; }是安全的,但*:focus { outline: none; }会连带废掉:focus-visible
调试时最常漏掉的三个检查点
写了样式却没反应?别急着改 CSS,先确认底层状态是否满足:
真正难的不是写对那两行 CSS,而是让所有可聚焦节点——包括自定义组件、编辑器容器、表单控件——在不同浏览器里都稳定响应键盘导航,并且不干扰鼠标用户的视觉流。Safari 下首次点击仍闪 outline 的问题,往往不是伪类本身的问题,而是 polyfill 初始化太晚,或者 tabindex 动态变更后未重置 UA 启发式判断上下文。











