navigator.maxtouchpoints 是判断触控能力最不可伪造的硬件信号,需与 matchmedia("(hover: none) and (pointer: coarse)") 等组合使用,动态响应用户输入意图,而非直接触发 ui 降级。

navigator.maxTouchPoints 不是“是不是手机”的开关,而是“能不能多点触控”的硬件信号。它本身不能直接触发 UI 降级,但它是整个判断链里最不可伪造的起点。
maxTouchPoints > 0 就该启用触摸 UI 吗?
不是。Surface 插着键盘用鼠标时,navigator.maxTouchPoints 仍可能返回 5 或 10;Windows 笔电驱动未加载时,也可能返回 0 即使有屏。硬编码 if (navigator.maxTouchPoints > 0) { enableTouchUI() } 会导致:桌面用户被迫面对超大按钮、悬停菜单消失、拖拽功能被禁用。
真正要响应的是「用户此刻倾向的输入方式」,而不是「设备有没有触摸能力」。所以应把它当作一个必要不充分条件,而非决策终点。
- 仅当
navigator.maxTouchPoints > 0且window.matchMedia("(hover: none) and (pointer: coarse)").matches为真时,才可较可靠地认定当前处于手持触控场景 - 若
matchMedia不支持(如旧版 Safari),退而使用"ontouchstart" in window做兼容判断,但需注意 polyfill 可能污染该值 - 永远不要在服务端或首次 JS 加载前就基于此值做样式注入——PWA 缓存下可能拿到过期状态
为什么不能只靠 matchMedia("(hover: none) and (pointer: coarse)")?
这个媒体查询在部分安卓 WebView 和早期 iOS 版本中不被支持;更关键的是,它在「折叠屏悬停态」或「二合一设备合盖用触控笔」等混合输入场景下会失效——此时 pointer 可能仍是 coarse,但 hover 却意外为 hover(因系统模拟了悬停行为)。
navigator.maxTouchPoints 在这种情况下反而更稳:只要硬件暴露了多点触控能力,它就不会撒谎。
- 推荐组合判断逻辑:
const isTouchIntent = mql.matches || (navigator.maxTouchPoints > 0 && !window.matchMedia("(hover: hover)").matches) - 监听
mql.addEventListener("change", handler),动态响应用户切换输入设备(比如拔掉鼠标、展开折叠屏) - 避免在组件挂载时只做一次判断——用户可能中途插拔外设
UI 降级不是加个 class 就完事
加 touch-mode 类只是第一步。真正的降级涉及交互语义迁移:PC 上的 mouseenter + click 组合,在触摸设备上必须退化为单点 touchstart,且要显式设置 { passive: false },否则滚动会被浏览器默认拦截。
- 移除所有依赖
mouseenter/mouseleave的下拉菜单逻辑,改用点击展开 - 禁用
:hover触发的动画(哪怕 CSS 里写了,JS 也得主动document.body.style.setProperty("--hover-enabled", "0")) - 右键菜单、拖拽排序、双击缩放等纯鼠标操作控件,在
isTouchIntent为真时应直接隐藏或替换为长按菜单 -
scroll-behavior: smooth在某些 Android 触摸滚动中会导致卡顿,建议降级为auto并用requestAnimationFrame手动控制滚动帧
maxTouchPoints === 0 时千万别放弃判断
这是最容易忽略的盲区。navigator.maxTouchPoints === 0 只代表「当前环境未报告触摸能力」,不等于「没有触摸屏」。常见于:Chrome 启用了 --disable-touch-events 标志、企业定制 ROM 屏蔽 API、远程桌面连接、某些 PWA 安装后沙箱限制。
此时必须启动回退链:
- 先查
"ontouchstart" in window—— 兼容性最好,但可能被第三方库覆盖 - 再 fallback 到
window.screen.width + <code>/(iPhone|Android)/i.test(navigator.userAgent)(仅作兜底,不用于主逻辑) - 最终策略:对关键交互组件(如滑块、轮播)保留双模式支持,运行时根据首次用户操作类型(
touchstartvsmousedown)动态锁定模式
复杂点在于:你永远无法 100% 确定用户下一秒会用手指还是鼠标。所以降级逻辑得留出「可逆通道」,比如在触摸模式下,检测到连续两次 mousemove 事件,就悄悄恢复 hover 样式和 drag 功能。










