:focus 响应所有聚焦方式,:focus-visible 仅响应键盘导航;必须先写 :focus 再写 :focus-visible 以确保兼容性;推荐使用 js-focus-visible polyfill 或渐进增强写法。

:focus 会响应所有聚焦方式,:focus-visible 只响应键盘导航
这是最根本的区别。只要元素获得焦点——不管是鼠标点、触摸 tap、JS 调用 focus(),还是按 Tab 键——:focus 都会生效;而 :focus-visible 是浏览器根据交互意图“猜”出来的:只有它判断为“用户正在键盘导航”时才匹配,比如按 Tab、Shift+Tab、方向键、Enter 或 Space。
这意味着:
- 鼠标点击一个
<button></button>后,button:focus一定触发,但button:focus-visible几乎从不触发(除非页面首次交互就是键盘) -
<input>被鼠标点开后光标闪烁,input:focus生效(需提供视觉反馈),但input:focus-visible不生效——所以不能只靠它控制输入框的聚焦态 - JS 手动调用
el.focus()时,:focus-visible是否触发取决于浏览器 UA 判断逻辑,不是确定行为;加{ focusVisible: true }仅 Chromium 支持,且需配合HTMLElement.focusOptions
必须先写 :focus,再写 :focus-visible,顺序不能反
因为同权重下后声明的规则会覆盖前声明的。如果把 :focus-visible 写在前面,旧浏览器直接忽略整条规则,而新浏览器里它又会覆盖掉后面的 :focus,结果是鼠标用户看不到任何焦点反馈,键盘用户在旧浏览器里也彻底失焦。
正确写法必须是:
button:focus {
outline: 2px solid #999;
}
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
这样:
- 所有浏览器都执行第一行,保证基础可访问性
- 支持
:focus-visible的浏览器(Chrome 86+、Firefox 86+、Safari 15.4+)会用第二行强化键盘用户的焦点环 - Safari 15.0–15.3 需手动开启辅助功能才启用原生
:focus-visible,行为不稳定,不能依赖
兼容性兜底不能靠 @supports selector(:focus-visible)
这个语法在 Safari 15.3 及更早版本中解析失败,整个 CSS 块会被丢弃——你连降级样式都拿不到。实际项目里,真正可行的路径只有两个:
- 引入 js-focus-visible Polyfill(约 2KB gzip),它在原生支持时静默退出
- 坚持「渐进增强」写法:
:focus兜底 +:focus-visible覆盖,且确保:focus永远写在前面
Polyfill 还要求你改写 CSS 规则为类选择器形式,例如:
button:focus:not(.js-focus-visible .focus-visible) {
outline: none;
}
button.js-focus-visible .focus-visible {
outline: 2px solid #0066cc;
}
否则旧版 Chrome 会回退到默认 outline,破坏设计一致性。
自定义元素和框架组件容易漏掉:focus-visible支持
不是加了 tabindex="0" 就自动有 :focus-visible 行为。浏览器只对语义化元素(<button></button>、<input>、<a href></a> 等)内置判断逻辑;对 <div role="button"> 或 React/Vue 封装的按钮,必须同时满足:
<ul>
<li>设 <code>tabindex="0" 且有明确 ARIA role(如 role="button")
keydown 并处理 Enter/Space,否则浏览器认为“不响应键盘”,跳过 :focus-visible 判断pointer-events: none 或 visibility: hidden 阻断聚焦链真机测试比 DevTools 模拟更重要——iOS Safari 对 input[type="number"] 的焦点延迟明显,建议统一用 type="text" 加输入限制。











