:focus 在任何获焦时触发,:focus-visible 仅键盘导航时生效;推荐用 &:focus:not(:focus-visible) + &:focus-visible 双层控制,并通过 CSS 变量适配深色模式与主题切换。

focus-visible 与 :focus 的语义差异必须分清
:focus 在任何获得焦点时都触发,包括键盘 Tab、JS el.focus()、甚至点击后自动聚焦的 。而 :focus-visible 是浏览器原生判断“用户是否正用键盘导航”的伪类,它能过滤掉鼠标/触屏点击带来的冗余焦点框——这是无障碍体验的关键分水岭。
不区分这两者,直接给所有 :focus 加粗边框或高亮背景,会导致:
- 鼠标用户看到突兀的视觉干扰(尤其在按钮上)
- 屏幕阅读器用户无法依赖一致的焦点反馈路径
- Chrome/Firefox/Safari 对
:focus-visible的支持已稳定(2026 年无需前缀),但旧版 Safari 仍需降级 fallback
推荐写法是双层覆盖:&:focus:not(:focus-visible) { outline: none; } + &:focus-visible { outline: 2px solid var(--focus-color); outline-offset: 2px; }
@mixin focus-ring 必须接收颜色、尺寸、偏移量三个参数
硬编码 outline 值的混合宏等于放弃主题适配能力。真正可复用的 @mixin focus-ring 应该只封装逻辑,不固化样式:
-
$color:必填,类型为color,默认取$primary-focus变量 -
$width:可选,单位必须是px或rem,避免使用em(会随字体缩放意外变粗) -
$offset:可选,负值会内收,正值外扩;设为0时易与 border 混淆,建议最小1px
示例:
@mixin focus-ring($color, $width: 2px, $offset: 2px) {
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: $width solid $color;
outline-offset: $offset;
}
}
深色模式下 focus-outline 不能只靠媒体查询硬切
@media (prefers-color-scheme: dark) 仅响应系统级设置,无法覆盖用户手动切换的主题(比如页面右上角的「日/夜」开关)。更稳妥的做法是用 CSS 自定义属性驱动:
- 在根节点声明:
:root { --focus-outline-color: #007bff; } - 深色主题 class(如
.theme-dark)里重置:.theme-dark { --focus-outline-color: #4dabf7; } - 混合宏中调用:
outline: $width solid var(--focus-outline-color);
这样,JS 切换主题时无需重新编译 SCSS,CSSOM 直接生效,且与 PurgeCSS 兼容。
focus 状态必须和 :active / :hover 分离测试
很多团队把 @mixin focus-ring 和 @mixin button-hover 写进同一个文件,结果导致:
-
button:focus:hover覆盖了focus-visible样式(因层叠权重更高) - 移动端双击触发
:active后,焦点框残留(iOS Safari 的经典 bug) - 测试时只点不按 Tab 键,漏掉键盘流路径
实操建议:
- 单独建
_focus.scss,只放与焦点逻辑相关的混合宏和重置规则 - 在组件文件中显式调用:
@include focus-ring(var(--brand-blue));,不混入其它交互状态 - 用真实键盘(非模拟)+ 屏幕阅读器(如 NVDA 或 VoiceOver)走一遍 Tab 流程,确认每一步都有明确、不闪烁的焦点指示
容易被忽略的是:focus-outline 的动画(如 outline: 2px solid transparent; transition: outline 0.15s;)在 Firefox 中可能被禁用,且对屏幕阅读器无意义。别加过渡,保持瞬时生效。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











