移动端 :hover 无自动退场机制,需用 @media (hover: hover) and (pointer: fine) 包裹鼠标样式,并以 :active 或 is-hovered 类适配触摸设备。

直接结论:不要指望移动端的 :hover 自动“退场”,它根本没设计退出机制;必须用 @media (hover: hover) and (pointer: fine) 包裹鼠标专属样式,并为触摸设备提供 :active 或 is-hovered 类替代方案。
为什么 :hover 在触摸后“卡住”不是 bug
iOS Safari 和 Android Chrome 把第一次 touchstart 当作 mouseover 触发 :hover,但没有对应的 mouseout 事件来清除它。滚动、切页、点空白处可能清掉,但不保证——真机上点完按钮背景色还挂着,就是这个原因。
常见错误现象包括:
- 下拉菜单点一下就展开又瞬间收起
- 按钮点击后
background-color残留数秒才消失 - 同一页面多个可悬停元素,点一个后其他 hover 样式也异常激活
@media (hover: hover) and (pointer: fine) 必须这么写
单独写 @media (hover: hover) 在 iPad、Surface 上恒为 true,完全不可靠;@media (hover)(缺值)整条规则会被浏览器忽略。
正确写法和要点:
- 必须带值:
@media (hover: hover) and (pointer: fine)才表示“当前主输入是精细指针(鼠标/触控笔),且支持悬停” - 必须放在常规样式之后,否则会被
.btn:hover直接覆盖 -
transition属性也要包进去,否则移动 Safari 可能解析到却不用 - 别写
@media (hover: hover) and (pointer: coarse)——W3C 规范里这两者互斥,永远不匹配
示例:
.btn {
background: #ccc;
}
@media (hover: hover) and (pointer: fine) {
.btn:hover {
background: #007bff;
transition: background 0.2s;
}
}
:active 是最稳的触摸反馈替代方案
:active 在手指按下瞬间触发、松开即失效,行为接近 PC 端的 :hover + :active 组合,但它在 iOS Safari 默认不生效。
让它生效的硬性条件(满足任一即可):
- 元素或其祖先绑定了
touchstart事件(最简做法:) - 元素是
<button></button>或带role="button"且有tabindex -
cursor: pointer在移动端纯 CSS 声明无效,不能依赖
实操建议:
-
:active动画持续时间 ≤ 0.15s,避免点击后有延迟感 - 别用
!important强行覆盖:hover,而是让:active自身定义完整状态(背景、缩放、边框等) - 父容器若设了
overflow: hidden,可能裁掉transform: scale()的缩放区域
用 JS 模拟 is-hovered 类时最容易漏掉的三件事
监听 touchstart 加 class、touchend 移 class 看似简单,但真实项目里高频出问题:
- 必须调用
e.preventDefault(),否则安卓部分机型会触发双击缩放或滚动中断 - 别在
touchend立即移除 class——用户滑动时touchend不一定触发;推荐用setTimeout(() => el.classList.remove('is-hovered'), 300) - 如果元素有跳转行为(如
<a href="/next"></a>),要在click回调里立即清除is-hovered,否则新页面样式被污染
更稳妥的做法是事件委托,统一在父容器监听,靠 event.target.matches('[data-hoverable]') 判断目标,避免单页应用中组件反复挂载导致的内存泄漏。
真正难的从来不是怎么写 :hover,而是怎么让它的边界在不同输入方式之间自然消失——它藏在 pointer: fine 的判定里,藏在 touchcancel 的监听里,也藏在你忘记给 <div> 加 <code>tabindex="0" 的那个瞬间。











