# Safari and every iOS browser

> On iPhone and iPad every browser runs Apple's WebKit, so WhiskerJS treats them all as the safari class and tells them apart with a few app-specific globals.

Source: https://whiskerjs.com/browsers/safari-ios/ (last updated 2026-10-26)

> **In plain words:**
>
> On an iPhone or iPad, every browser (Safari, Chrome, Firefox, Edge and others) is built on Apple’s WebKit engine. To WhiskerJS they all look like Safari, so they share Safari’s strengths and gaps: no device memory or Client Hints, but Apple-specific signals such as Apple Pay. The apps differ in small ways, for example in a few hidden variables they add to the page, and WhiskerJS reports those so your server can tell the apps apart.

## At a glance

- Browser class: `safari`, for Safari and for Chrome, Firefox and Edge on iOS; `safari_afp` when Advanced Fingerprinting Protection alters canvas or WebGL readback (for example in private browsing).
- Excluded components: none in the `safari` class; 5 under `safari_afp` (`audio`, `canvas`, `screen_frame`, `screen_resolution`, `webgl_image`).
- CPU threads: a 4 or an 8 is reported as the bucket `4_8`.
- Unavailable by design: `device_memory`, `heap_limit`, `ua_client_hints`, `ua_versions`, `keyboard_layout`.
- Read here: `audio_latency` (WebKit classes), `touch_support` with touch points.
- Telling apps apart: `vendor_flavors`.
- Lab sample: iPhone Chrome (3 runs) and iPhone Safari (3 runs) each kept one id at signalsVersion 1.

## Why every iOS browser is classed safari

The class test in `src/browser-class.ts` runs in a fixed order, and on iOS each step falls through to `safari`:

1. **Not `brave`**: unless `navigator.brave.isBrave` is a function.
2. **Not `firefox`**: Firefox on iOS uses `FxiOS/` in its user agent, not a `Firefox/<number>` token, and WebKit has no `mozInnerScreenX`.
3. **Not `chromium`**: Chrome on iOS uses `CriOS/` and Edge on iOS `EdgiOS/`, not a `Chrome/<number>` token, and the vendor is not `Google Inc.`.
4. **`safari`**: `navigator.vendor` is `Apple Computer, Inc.` in every WebKit browser.

This is the right result for the exclusion table and for the engine check: the code really runs on WebKit. The integrity check [`engine_mismatch`](https://whiskerjs.com/docs/integrity/#engine_mismatch) expects WebKit behaviour (a 37-character `eval` source and `productSub` `20030107`), which these browsers have. And [`headless_markers`](https://whiskerjs.com/docs/integrity/#headless_markers) does not apply its “desktop Chrome without `window.chrome`” test to user agents with `CriOS`, `Mobile` or `Android`.

## Telling iOS browsers apart

[`vendor_flavors`](https://whiskerjs.com/signals/vendor-flavors/) reports which of ten browser-specific globals exist on the page. Three of them are linked to iOS apps: `__crWeb` and `__gCrWeb` (associated with Chrome on iOS) and `__firefox__` (associated with Firefox on iOS). Whether an app defines them depends on the app and its version, so treat the value as a hint that differs between apps, not as a guarantee. WhiskerJS checks no Edge-specific global. The component is part of the `browser` group of the visitorId, which is one reason Safari and Chrome on the same iPhone can have different ids.

## What works

- [`audio_latency`](https://whiskerjs.com/signals/audio-latency/): read because the class is `safari` or `safari_afp` (a WebKit class); the `AudioContext` is closed at once and nothing is played.
- [`touch_support`](https://whiskerjs.com/signals/touch-support/): touch points and touch events; [`screen_resolution`](https://whiskerjs.com/signals/screen-resolution/) with the larger side first, so rotation does not change it.
- [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) and [`private_click`](https://whiskerjs.com/signals/private-click/) in Safari; in other iOS apps they depend on what the app’s WebKit exposes, and they report the plain value `none` when the API is missing.
- Canvas, WebGL, offline audio, fonts, font preferences, Math, codecs, locale and display preferences.

## What is unavailable or blocked

| Component | Status | Why |
| --- | --- | --- |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | `unavailable` / `no_api` | WebKit has no `navigator.deviceMemory` |
| [`heap_limit`](https://whiskerjs.com/signals/heap-limit/) | `unavailable` / `no_api` | no `performance.memory` |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/), [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | `unavailable` / `no_api` | no `navigator.userAgentData`, even in Chrome on iOS |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | `unavailable` / `no_api` | no `navigator.keyboard` |

[`plugins`](https://whiskerjs.com/signals/plugins/) often lists nothing on mobile and then reports `none`; [`pdf_viewer`](https://whiskerjs.com/signals/pdf-viewer/) is `unavailable` where `navigator.pdfViewerEnabled` does not exist. [`webgpu`](https://whiskerjs.com/signals/webgpu/) depends on the iOS version.

## What WhiskerJS excludes and why

Nothing in the `safari` class; that row of the exclusion table is empty. As on the Mac, Advanced Fingerprinting Protection (on in private browsing, and wherever the setting says so) adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. When the canvas or WebGL [known-answer probe](https://whiskerjs.com/signals/noise-profile/) sees that noise, the class is `safari_afp` and `audio`, `canvas`, `screen_frame`, `screen_resolution` and `webgl_image` are `excluded` with ruleId `safari_afp`. Noise that no row covers still turns `canvas`, `webgl_image` or `audio` `noisy` (`render_mismatch` from the [double render](https://whiskerjs.com/glossary/#double-render), or `seeded_noise` from the known-answer probes). See [Safari on macOS](https://whiskerjs.com/browsers/safari-macos/#what-whiskerjs-excludes-and-why).

[`hardware_concurrency`](https://whiskerjs.com/signals/hardware-concurrency/) is read 16 times and a 4 or an 8 is reported as `4_8`: WebKit alternates between 4 and 8 for scripts on its known-fingerprinting list, also between page loads, and the bucket keeps the id steady.

## Integrity flags to expect

- [`iframe_mismatch`](https://whiskerjs.com/docs/integrity/#iframe_mismatch) no longer fires for WebKit’s 0 by 0 screen in a hidden iframe: since signalsVersion 9 screen values are compared only when the iframe reports a screen wider than 0.
- [`automation_webdriver`](https://whiskerjs.com/docs/integrity/#automation_webdriver) in device test farms driven through WebDriver.
- [`native_tampering`](https://whiskerjs.com/docs/integrity/#native_tampering) and related lie checks when privacy extensions or other scripts on the page override functions.

## Early lab observations

An early, small sample at signalsVersion 1, not a benchmark:

| Device and browser | Runs | Ids seen |
| --- | --- | --- |
| iPhone, Chrome | 3 | 1 |
| iPhone, Safari | 3 | 1 |

Each kept one id across its runs, and the two ids differed from each other and from the two Mac ids in the sample. `device_memory` was `unavailable` on both, including Chrome on iPhone, because Chrome there runs on WebKit. The sample used 30 components, not the current 46.

## Testing it yourself

Open the [live demo](https://whiskerjs.com/demo/) in two iOS browsers on the same device and compare the structuredId groups: the `browser` group is where they are most likely to differ. The [lab mode](https://whiskerjs.com/demo/#lab) copies one line per run. Nothing is sent anywhere.

## Support table for Safari and every iOS browser

Expected status of every component in this browser at signalsVersion 9, from [the support matrix](https://whiskerjs.com/browsers/support-matrix/) (updated 2026-10-26). Basis "code" means expected from the library code and documentation, not yet measured; "lab" means seen in the early lab sample.

| Component | Status | Basis | Note |
| --- | --- | --- | --- |
| [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) | varies | code | Safari has ApplePaySession; in other iOS browsers it depends on the app. none where it is missing. |
| [`audio`](https://whiskerjs.com/signals/audio/) | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
| [`audio_latency`](https://whiskerjs.com/signals/audio-latency/) | ok | code | Read for browser class safari; the context is closed at once. |
| [`browser_class`](https://whiskerjs.com/signals/browser-class/) | ok | code | Class safari, for every iOS browser (Apple vendor, no Chrome/ token); safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback. |
| [`canvas`](https://whiskerjs.com/signals/canvas/) | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
| [`canvas_readback`](https://whiskerjs.com/signals/canvas-readback/) | varies | code | exact, or altered with Advanced Fingerprinting Protection (always in private browsing). |
| [`color_depth`](https://whiskerjs.com/signals/color-depth/) | ok | code |   |
| [`color_gamut`](https://whiskerjs.com/signals/color-gamut/) | ok | code |   |
| [`content_blockers`](https://whiskerjs.com/signals/content-blockers/) | ok | code | none, or the bait names a content blocker hid. |
| [`cpu_architecture`](https://whiskerjs.com/signals/cpu-architecture/) | ok | code |   |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | unavailable | lab | Seen unavailable on iPhone Safari and iPhone Chrome in the lab sample; no iOS browser offers it. |
| [`dynamic_range`](https://whiskerjs.com/signals/dynamic-range/) | ok | code |   |
| [`font_preferences`](https://whiskerjs.com/signals/font-preferences/) | ok | code |   |
| [`fonts`](https://whiskerjs.com/signals/fonts/) | ok | code |   |
| [`fonts_canvas`](https://whiskerjs.com/signals/fonts-canvas/) | ok | code |   |
| [`forced_colors`](https://whiskerjs.com/signals/forced-colors/) | ok | code |   |
| [`hardware_concurrency`](https://whiskerjs.com/signals/hardware-concurrency/) | ok | code | Read 16 times; a 4 or an 8 is reported as the bucket 4_8, because WebKit alternates between them for scripts on its known-fingerprinting list. |
| [`heap_limit`](https://whiskerjs.com/signals/heap-limit/) | unavailable | code | performance.memory is a Chromium API. Reports unavailable / no_api. |
| [`intl_locale`](https://whiskerjs.com/signals/intl-locale/) | ok | code |   |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | unavailable | code | No navigator.keyboard here: unavailable / no_api. |
| [`languages`](https://whiskerjs.com/signals/languages/) | ok | code |   |
| [`math`](https://whiskerjs.com/signals/math/) | ok | code |   |
| [`media_codecs`](https://whiskerjs.com/signals/media-codecs/) | ok | code |   |
| [`noise_profile`](https://whiskerjs.com/signals/noise-profile/) | ok | code | exact in a normal window; seeded for the APIs Advanced Fingerprinting Protection randomises. |
| [`pdf_viewer`](https://whiskerjs.com/signals/pdf-viewer/) | varies | code | unavailable where navigator.pdfViewerEnabled does not exist. |
| [`permissions`](https://whiskerjs.com/signals/permissions/) | ok | code |   |
| [`pixel_ratio`](https://whiskerjs.com/signals/pixel-ratio/) | ok | code |   |
| [`platform`](https://whiskerjs.com/signals/platform/) | ok | code |   |
| [`plugins`](https://whiskerjs.com/signals/plugins/) | varies | code | Mobile builds often list no plugins and then report the plain value none. |
| [`prefers_contrast`](https://whiskerjs.com/signals/prefers-contrast/) | ok | code |   |
| [`private_click`](https://whiskerjs.com/signals/private-click/) | varies | code | Depends on whether the app's WebKit exposes Private Click Measurement; none otherwise. |
| [`reduced_motion`](https://whiskerjs.com/signals/reduced-motion/) | ok | code |   |
| [`screen_frame`](https://whiskerjs.com/signals/screen-frame/) | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
| [`screen_resolution`](https://whiskerjs.com/signals/screen-resolution/) | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
| [`speech_voices`](https://whiskerjs.com/signals/speech-voices/) | ok | code | Digest of the voice list, or none; some browsers fill the list late. |
| [`storage`](https://whiskerjs.com/signals/storage/) | ok | code |   |
| [`time_zone`](https://whiskerjs.com/signals/time-zone/) | ok | code |   |
| [`touch_support`](https://whiskerjs.com/signals/touch-support/) | ok | code |   |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
| [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
| [`vendor`](https://whiskerjs.com/signals/vendor/) | ok | code |   |
| [`vendor_flavors`](https://whiskerjs.com/signals/vendor-flavors/) | ok | code |   |
| [`webgl_image`](https://whiskerjs.com/signals/webgl-image/) | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
| [`webgl_params`](https://whiskerjs.com/signals/webgl-params/) | ok | code | Where WebGL 1 is available; unavailable / no_context without it. |
| [`webgl_renderer`](https://whiskerjs.com/signals/webgl-renderer/) | ok | code | Often a generic string such as Apple GPU. |
| [`webgl_vendor`](https://whiskerjs.com/signals/webgl-vendor/) | ok | code |   |
| [`webgpu`](https://whiskerjs.com/signals/webgpu/) | varies | code | Depends on browser version, operating system and GPU; needs a secure context. unavailable / no_api without navigator.gpu, no_context without an adapter. |

## Questions and answers

### Why does Chrome on my iPhone show browser_class safari?

On iOS every browser uses WebKit. Chrome on iOS reports the Apple vendor string and its user agent carries CriOS instead of a Chrome/ token, so the class test matches safari. That is expected, not a bug.

### Do Safari and Chrome on the same iPhone get the same id?

Not necessarily. In the early lab sample, iPhone Chrome and iPhone Safari each kept their own id, and the ids differed. vendor_flavors is one component that can differ between iOS browsers.

### Why is device_memory unavailable on every iPhone browser?

No iOS browser offers navigator.deviceMemory, because WebKit does not. The lab sample showed it unavailable on iPhone Safari and iPhone Chrome.

## Related pages

- [Safari on macOS](https://whiskerjs.com/browsers/safari-macos/): The same engine on the Mac.
- [vendor_flavors](https://whiskerjs.com/signals/vendor-flavors/): The browser-specific globals WhiskerJS checks.
- [browser_class](https://whiskerjs.com/signals/browser-class/): How the class is chosen.
- [device_memory](https://whiskerjs.com/signals/device-memory/): A Chromium-only API.
- [Browser support matrix](https://whiskerjs.com/browsers/support-matrix/): All browsers side by side.
