Skip to content

Modernize isMobile / platform detection — UA sniffing misses iPadOS 13+, includes dead platforms #1467

@obiot

Description

@obiot

While adding test coverage for src/system/platform.ts, audited the platform-detection constants. They're functional but the underlying user-agent regex approach has known gaps in 2026, the biggest being that modern iPads identify as desktop Macs.

Current implementation

// src/system/platform.ts
export const iOS = /iPhone|iPad|iPod/i.test(ua);
export const android = /Android/i.test(ua);
export const wp = /Windows Phone/i.test(ua);
export const BlackBerry = /BlackBerry/i.test(ua);
export const Kindle = /Kindle|Silk.*Mobile Safari/i.test(ua);
export const isMobile =
    /Mobi/i.test(ua) || iOS || android || wp || BlackBerry || Kindle;

What's outdated / wrong

1. iPadOS 13+ ships as Macintosh (the big one)

Since iPadOS 13 (2019), Safari's default UA on iPad is Mozilla/5.0 (Macintosh; Intel Mac OS X ...)no iPad token. The iOS regex misses every modern iPad, and they fall through isMobile as desktop. This is a real, observable gap for our keyboard/input branches (src/input/keyboard.ts consumes isMobile).

Standard fix: also check

navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1

(a Mac with multi-touch is, in practice, always an iPad — Macs don't have touchscreens).

2. Dead platforms still scanned

  • wp (Windows Phone) — Microsoft EOL'd the platform in 2017. The regex matches a UA string nothing ships anymore.
  • BlackBerry — RIM stopped making BB10 devices in 2016; UA appears in <0.01% of traffic globally.
  • android2 — exported but unused; Android 2.x is from 2009-2011.

These can probably be deleted (or at least dropped from isMobile's OR chain).

3. UA sniffing is the wrong primitive

The web platform has caught up with this since the original detection was written:

  • navigator.maxTouchPoints> 0 means the device has a touch digitizer
  • matchMedia(\"(pointer: coarse)\") — touch is the primary pointer
  • navigator.userAgentData — Client Hints API, gives structured browser/platform/mobility info on Chromium-based browsers (matches Sec-CH-UA-Mobile). Limited Safari/Firefox support today but the right long-term direction.

For our use cases (skip on-screen keyboard branch, pick gamepad-vs-touch input, set viewport scaling defaults), feature detection is more correct than UA detection — a touchscreen laptop should be treated as having touch capability without being "mobile".

Suggested approach

Roughly in tiers (don't have to do all at once):

  1. Quick win: patch iOS to also catch the iPadOS-13-as-Mac case via navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1. One-line fix, closes the biggest gap.

  2. Add an isTouch flag (separate from isMobile) — matchMedia(\"(pointer: coarse)\").matches || (navigator.maxTouchPoints || 0) > 0. This is what most internal call sites probably actually want.

  3. Deprecate the dead platformswp, BlackBerry, Kindle, android2 — leave the exports for backwards-compat through 19.x, mark deprecated, remove in 20.x.

  4. Keep isMobile as a UA-based heuristic but document that it's best-effort and recommend isTouch / pointer: coarse for new code.

  5. Long-term: migrate to navigator.userAgentData where available, fall back to UA. Out of scope for a single PR.

Test coverage

The new tests/platform.spec.ts (added in the same audit pass) pins the current behavior under Playwright + chromium. Any modernization PR should update those expectations and add cases for the iPad-on-iPadOS scenario (which the chromium test runner can simulate by overriding navigator.platform + maxTouchPoints).

🤖 Filed via Claude Code during the 19.7 / Camera3d cleanup pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions