必须用 js-focus-visible Polyfill,因旧版浏览器不解析 :focus-visible 且 :focus 无法区分输入方式,会导致鼠标点击误显 outline 并违反可访问性;Polyfill 体积小、兼容 IE11 起、自动降级,但需配合类选择器 CSS 和 tabindex/role/keydown 三要素才能生效。

必须用 js-focus-visible Polyfill,不能靠 CSS 降级或手写 JS 模拟。旧版浏览器(Chrome 86 以下、IE 全系、Safari 15.3 及更早)压根不解析 :focus-visible 这个语法,整条规则被忽略——不是样式没生效,是浏览器根本没读它。
为什么不能只写 :focus 当兜底?
直接把 :focus-visible 替成 :focus 会导致鼠标点击按钮时也显示 outline,干扰视觉动效;更重要的是违背可访问性原则:键盘用户需要明确焦点提示,鼠标用户不需要。两者语义完全不同——:focus 只管“有没有焦点”,:focus-visible 管“怎么获得的焦点”。
常见错误现象包括:
- 鼠标点完按钮,看到难看的蓝色外框,但设计稿里不该出现
- 键盘用户 Tab 到输入框,却没任何视觉反馈(因为全局写了
* { outline: none; },又没给:focus-visible补回) - React Button 或自定义组件完全不响应
:focus-visible,键盘导航时失焦
必须用 js-focus-visible 的三个硬原因
它不是“可选方案”,而是目前唯一能覆盖全场景的实现:
立即学习“前端免费学习笔记(深入)”;
- 体积小(约 2KB gzip),不依赖
MutationObserver,从 IE11 到 Chrome 49 都能跑 - 核心逻辑可靠:监听首次键盘操作(Tab、Shift+Tab、方向键等),在
<html>上加js-focus-visible类,并给当前聚焦元素加focus-visible类 - 支持自动退出机制:现代浏览器原生支持
:focus-visible时,Polyfill 静默不执行,不重复干预
引入方式很简单:<script src="https://unpkg.com/js-focus-visible@5.0.0/dist/js-focus-visible.min.js"></script>
或者在 Webpack/Vite 中 import 'js-focus-visible' 并确保 sideEffects: false,避免被 tree-shaking 误删。
CSS 必须重写为类选择器 + 降级规则
原生写法 button:focus-visible 在旧浏览器里无效,必须改成类选择器形式:
button:focus-visible,
button.js-focus-visible .focus-visible {
outline: 2px solid #007aff;
}同时必须加降级规则,否则旧版 Chrome 会回退到默认 outline:
button:focus:not(.js-focus-visible .focus-visible) {
outline: none;
}注意::focus 声明必须在前,:focus-visible 或类选择器在后;否则不支持的浏览器连基础 :focus 样式都可能被覆盖失效。
自定义元素漏掉这三步,Polyfill 就白加
即使引入了 js-focus-visible,<div>、封装组件、Web Component 也不会自动获得行为。必须同时满足:
- 设
tabindex="0" - 有明确 ARIA role(如
role="button") - 监听
onkeydown并处理 Enter/Space,否则浏览器认为“不响应键盘”,跳过判断
特别注意移动端 Safari:JS 调用 .focus() 且无真实用户手势时,focus 事件本身不会触发——js-focus-visible 内部已处理,手写逻辑几乎不可能覆盖这点。
真正难的从来不是加一行 <script>,而是确保所有可聚焦元素——包括 React Button、tabindex="0" 的 <div>、Shadow DOM 里的控件——都被正确识别。漏掉任意一个,键盘用户的焦点链就断了。


















