:hover和:active不应映射为bem修饰符,因其是瞬时、不可控的原生交互状态;应由伪类定义视觉反馈,修饰符仅表达可编程的静态变体或可控状态,如--disabled、--loading、--elevated等。

伪类:hover和:active不该直接映射成BEM修饰符
直接写 .btn--hover 或 .btn--active 是反模式。BEM 修饰符描述的是组件的「静态变体」或「可编程状态」,而 :hover 和 :active 是浏览器原生触发的瞬时交互状态,不经过 JS 控制、不可序列化、无法被自动化测试稳定捕获。
常见错误现象:
- 加了
btn--hover类但鼠标没动,样式却一直生效 - 键盘用户聚焦按钮时,
btn--hover完全不响应,无障碍体验断裂 - 用 Jest 测试 hover 行为时,必须 mock
dispatchEvent,成本高且易漏
正确做法是把视觉反馈逻辑交给伪类,把「可控状态」留给修饰符:
-
.btn:hover定义悬停效果(颜色、阴影、位移) -
.btn:active定义按下反馈(缩放、背景加深、transform微调) -
.btn--disabled:hover显式覆盖,禁用悬停行为 -
.btn--loading这类修饰符只管图标、文字、pointer-events,不碰:hover逻辑
需要 JS 控制的“悬停态”该用什么修饰符
只有当悬停行为本身受业务逻辑约束时,才该引入修饰符——比如权限关闭后连悬停反馈都要消失,或者某卡片在编辑模式下禁止所有交互。
这时修饰符名不能叫 --hover,而应表达结果而非条件:
-
.card--elevated:表示当前处于“已提升”状态(可能来自 hover、focus 或 JS 主动设置) -
.card--interactive:表示组件整体支持交互(由权限/模式决定),影响:hover是否生效 -
.btn--no-interaction:比--disabled更轻量,仅移除视觉反馈,不阻断表单提交
关键点:
- 修饰符必须能独立存在,删掉它 UI 就回退到默认态
- JS 统一增删类,不依赖事件类型(
mouseenter/focus都触发同一 class) - CSS 中不写
.card--elevated:hover,只定义.card--elevated的最终样式
如何让修饰符同时响应鼠标和键盘焦点
纯 CSS 无法可靠判断“当前是否被聚焦且未悬停”,所以得靠 JS 协调状态。核心不是写两套 class,而是统一入口、分层响应。
实操建议:
- 给元素加
tabindex="0",确保可聚焦 - 监听
mouseenter/mouseleave和focus/blur - 用
element.matches(':hover')+document.hasFocus()判断是否需保留状态 - 避免
mouseleave立即移除 class,先检查是否仍聚焦:card.addEventListener('mouseleave', () => {<br> if (!card.matches(':focus')) {<br> card.classList.remove('card--elevated');<br> }<br>});
这样既兼容鼠标操作,又不破坏键盘导航流,还能让屏幕阅读器正确播报状态变化。
BEM修饰符与伪类共存时的样式优先级陷阱
很多人写 .btn--primary:hover,以为只是“加强版 hover”,实际埋下三个隐患:
- 复用困难:
.link--primary:hover得重写一遍,违背 BEM 可组合初衷 - 权重失控:如果基础
.btn:hover在后声明,.btn--primary:hover可能被意外覆盖 - 语义混淆:
--primary是颜色主题,不是交互状态,混在一起会让团队误读设计意图
更可持续的做法是分离关注点:
- 修饰符只管静态外观:
.btn--primary { background-color: #007bff; } - 伪类统一接管反馈:
.btn:hover { transform: translateY(-2px); } - 例外覆盖只在必要时:
.btn--disabled:hover { cursor: not-allowed; }
真正容易被忽略的是:一旦你开始用 JS 控制 --elevated 这类修饰符,就必须同步处理触摸设备的 touchstart / touchend,否则移动端用户会感觉“没反应”。这不是伪类能自动覆盖的边界。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











