移动端无真正悬停,:hover仅单向触发且不自动退出;需用@media (hover: hover) and (pointer: fine)精准识别真悬停设备,配合:active或JS模拟is-hovered类并注意touchcancel、延迟清除等细节。

因为移动端没有真正的悬停状态,浏览器只是“假装支持” :hover,但只管开始、不管结束——点一下就进 :hover,却没事件告诉它“该退出了”。
为什么点完按钮背景色还挂着不消失
iOS Safari 和部分 Android Chrome 会把首次触摸当作一次 :hover 的起点,随后立即进入该状态;但手指抬起时,并不会触发等效于 mouseout 的清除逻辑。滚动、切页、点空白处可能偶然清掉,但不保证,也不及时。
-
:hover在触摸设备上本质是“一次性模拟”,不是持续状态 - 设备上报
hover: hover为 true(为了兼容旧站),但实际输入是pointer: coarse(手指) - 媒体查询若只写
@media (hover: hover),在 iPad 或 Surface Pro 上恒成立,完全不可靠
@media (hover: hover) and (pointer: fine) 才是真正可用的守门员
这个组合才是 W3C 规范里唯一能区分“真悬停设备”的方式:既支持悬停,又输入足够精细(鼠标、触控笔),自动排除所有手指触控场景。
- 必须两个条件同时满足,缺一不可;
@media (hover: hover) and (pointer: coarse)永远不匹配(规范互斥) -
transition属性也得包进这个媒体查询里,否则移动 Safari 可能解析到却不用 - 顺序要放在常规样式之后,否则会被
.btn:hover直接覆盖
:active 是移动端最稳的瞬时反馈替代方案
:active 在 iOS Safari 默认不触发,除非你主动“激活”它——不是靠 CSS,而是靠 DOM 行为或语义。
立即学习“前端免费学习笔记(深入)”;
- 元素或其祖先绑定了
touchstart事件(最简写法:<body ontouchstart="">) - 元素本身是
<button>,或带role="button"且有tabindex="0" -
:active动画建议 ≤ 0.15s;别只改背景色,组合transform: scale(0.98)+opacity更可靠
JS 模拟 is-hovered 类时最容易漏掉的三件事
监听 touchstart/touchend 看似直接,但真实场景中极易出错。
- 必须加
e.preventDefault(),否则安卓部分机型会触发双击缩放或滚动中断 - 别在
touchend立即移除类——用户滑动时touchend不一定触发,推荐用setTimeout(..., 300)延迟清除 - 若元素有跳转行为(如
<a href>),要在click回调里立即清除is-hovered,否则新页面样式被污染
真正难的从来不是怎么写 :hover,而是怎么让它的边界在不同输入方式之间自然消失——它藏在 pointer: fine 的判定里,藏在没监听 touchcancel 导致的状态残留里,也藏在你忘了给父容器加 will-change: transform 的那一行注释里。


















