# Signals

> All 46 components: what each one reads, why it helps and what a value looks like.

Source: https://whiskerjs.com/docs/signals/ (last updated 2026-10-26)

> **In plain words:**
>
> A signal (or component) is one measurement, such as the screen size or the list of installed fonts. Each one differs a little between devices; together they tell devices apart. 37 signals rarely change and form the visitorId, sorted into seven groups (graphics, audio, fonts, device, browser, locale, preferences). Nine change too easily (zoom, taskbar, permissions and so on), so they are reported but kept out of the ids.

Every component also has its own page, with how the technique works, stability, browser support, privacy and every status it can report: see [Signals](https://whiskerjs.com/signals/) and the [glossary](https://whiskerjs.com/glossary/).

WhiskerJS reports 46 components. 37 of them are **stable** and make up the visitorId; each stable component belongs to exactly one of seven groups (the parts of the structuredId). The other 9 are reported for your server to use but kept out of both ids, because they change too easily.

How to read each entry:

- **Reads**: the browser API and exactly what is taken from it.
- **Why it helps**: what makes it differ between devices.
- **Typical value**: the shape of an `ok` value. Values shown as “digest” are 32 hex characters (see [How it works](https://whiskerjs.com/docs/how-it-works/#hashing)).
- **Support**: where it is usually `ok`, and what it reports elsewhere.

Values in the examples are illustrative, not from a particular device.

## Overview

| Component | Group | In visitorId | Always hashed |
| --- | --- | --- | --- |
| [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) | browser | yes | no |
| [`audio`](https://whiskerjs.com/signals/audio/) | audio | yes | yes |
| [`audio_latency`](https://whiskerjs.com/signals/audio-latency/) | none | no | no |
| [`browser_class`](https://whiskerjs.com/signals/browser-class/) | browser | yes | no |
| [`canvas`](https://whiskerjs.com/signals/canvas/) | gpu | yes | yes |
| [`canvas_readback`](https://whiskerjs.com/signals/canvas-readback/) | none | no | no |
| [`color_depth`](https://whiskerjs.com/signals/color-depth/) | device | yes | no |
| [`color_gamut`](https://whiskerjs.com/signals/color-gamut/) | prefs | yes | no |
| [`content_blockers`](https://whiskerjs.com/signals/content-blockers/) | none | no | no |
| [`cpu_architecture`](https://whiskerjs.com/signals/cpu-architecture/) | device | yes | no |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | device | yes | no |
| [`dynamic_range`](https://whiskerjs.com/signals/dynamic-range/) | prefs | yes | no |
| [`font_preferences`](https://whiskerjs.com/signals/font-preferences/) | fonts | yes | yes |
| [`fonts`](https://whiskerjs.com/signals/fonts/) | fonts | yes | yes (`none` plain) |
| [`fonts_canvas`](https://whiskerjs.com/signals/fonts-canvas/) | fonts | yes | yes (`none` plain) |
| [`forced_colors`](https://whiskerjs.com/signals/forced-colors/) | prefs | yes | no |
| [`hardware_concurrency`](https://whiskerjs.com/signals/hardware-concurrency/) | device | yes | no |
| [`heap_limit`](https://whiskerjs.com/signals/heap-limit/) | device | yes | no |
| [`intl_locale`](https://whiskerjs.com/signals/intl-locale/) | locale | yes | no |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | none | no | no |
| [`languages`](https://whiskerjs.com/signals/languages/) | locale | yes | no |
| [`math`](https://whiskerjs.com/signals/math/) | browser | yes | yes |
| [`media_codecs`](https://whiskerjs.com/signals/media-codecs/) | browser | yes | yes |
| [`pdf_viewer`](https://whiskerjs.com/signals/pdf-viewer/) | browser | yes | no |
| [`permissions`](https://whiskerjs.com/signals/permissions/) | none | no | yes |
| [`pixel_ratio`](https://whiskerjs.com/signals/pixel-ratio/) | none | no | no |
| [`platform`](https://whiskerjs.com/signals/platform/) | browser | yes | no |
| [`plugins`](https://whiskerjs.com/signals/plugins/) | browser | yes | yes (`none` plain) |
| [`prefers_contrast`](https://whiskerjs.com/signals/prefers-contrast/) | prefs | yes | no |
| [`private_click`](https://whiskerjs.com/signals/private-click/) | browser | yes | no |
| [`reduced_motion`](https://whiskerjs.com/signals/reduced-motion/) | prefs | yes | no |
| [`screen_frame`](https://whiskerjs.com/signals/screen-frame/) | none | no | no |
| [`screen_resolution`](https://whiskerjs.com/signals/screen-resolution/) | device | yes | no |
| [`speech_voices`](https://whiskerjs.com/signals/speech-voices/) | none | no | yes (`none` plain) |
| [`storage`](https://whiskerjs.com/signals/storage/) | browser | yes | no |
| [`time_zone`](https://whiskerjs.com/signals/time-zone/) | locale | yes | no |
| [`touch_support`](https://whiskerjs.com/signals/touch-support/) | device | yes | no |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/) | browser | yes | no |
| [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | none | no | no |
| [`vendor`](https://whiskerjs.com/signals/vendor/) | browser | yes | no |
| [`vendor_flavors`](https://whiskerjs.com/signals/vendor-flavors/) | browser | yes | no |
| [`webgl_image`](https://whiskerjs.com/signals/webgl-image/) | gpu | yes | yes |
| [`webgl_params`](https://whiskerjs.com/signals/webgl-params/) | gpu | yes | yes |
| [`webgl_renderer`](https://whiskerjs.com/signals/webgl-renderer/) | gpu | yes | no |
| [`webgl_vendor`](https://whiskerjs.com/signals/webgl-vendor/) | gpu | yes | no |
| [`webgpu`](https://whiskerjs.com/signals/webgpu/) | gpu | yes | yes |

“No” in the last column still means: hashed when the value is longer than 128 characters or not printable ASCII.

The exclusion table removes some of these under Brave and Firefox resistFingerprinting; see [How it works](https://whiskerjs.com/docs/how-it-works/#the-exclusion-table).

## gpu: graphics

Graphics output depends on the GPU, its driver, the operating system and the browser’s rendering engine together, which makes this the most distinguishing group and also the one that changes after a driver update.

### `canvas`

- **Reads**: a 260x80 2D canvas scene (a filled rectangle, the text “Whiskers twitch, jumpy fox” with a cat emoji in Times New Roman and Arial, three circles with `multiply` blending, an even-odd ring), exported with `toDataURL()`. Rendered twice.
- **Why it helps**: font rasterising, anti-aliasing, emoji images and blending differ between systems.
- **Typical value**: digest.
- **Support**: all browsers. `noisy` / `render_mismatch` where readback is randomised; `excluded` under Brave and Firefox resistFingerprinting.

### `webgl_image`

- **Reads**: a 32x32 WebGL drawing of one triangle with a procedural colour shader, read back with `readPixels()`. Drawn twice.
- **Why it helps**: shader maths and rasterising differ between GPUs and drivers.
- **Typical value**: digest.
- **Support**: where WebGL 1 is available. `unavailable` / `no_context` without WebGL; `noisy` when readback is randomised; `excluded` under Brave and Firefox resistFingerprinting.

### `webgl_params`

- **Reads**: 21 WebGL parameters (texture sizes, viewport limits, bit depths, shading language version and others), the range and precision of four float and int formats in both shader types, and the sorted list of supported extensions.
- **Why it helps**: limits and extensions follow the GPU, driver and browser.
- **Typical value**: digest.
- **Support**: where WebGL 1 is available.

### `webgl_renderer`

- **Reads**: the unmasked renderer string from `WEBGL_debug_renderer_info` where the extension exists, otherwise `gl.RENDERER`. In Firefox the extension is not requested (it prints a deprecation warning there) and `gl.RENDERER` already carries the unmasked string.
- **Why it helps**: names the GPU, often with the graphics back end.
- **Typical value**: `ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)`; hashed when longer than 128 characters.
- **Support**: where WebGL 1 is available. `excluded` under Firefox resistFingerprinting. A software renderer here also sets the `software_renderer` flag.

### `webgl_vendor`

- **Reads**: the unmasked vendor string, same method as `webgl_renderer`.
- **Typical value**: `Google Inc. (Apple)`.
- **Support**: as `webgl_renderer`.

### `webgpu`

- **Reads**: `navigator.gpu.requestAdapter()`, then the adapter’s `info` (vendor, architecture, device, description), its sorted feature list and seven limits (`maxBindGroups`, `maxBufferSize`, `maxComputeInvocationsPerWorkgroup`, `maxComputeWorkgroupStorageSize`, `maxStorageBufferBindingSize`, `maxTextureDimension2D`, `maxVertexBuffers`). No device is requested, and the deprecated `requestAdapterInfo()` (which warns in the console) is not called.
- **Why it helps**: a second, newer view of the GPU.
- **Typical value**: digest.
- **Support**: browsers with WebGPU, in secure contexts. `unavailable` / `no_api` without `navigator.gpu`, `no_context` when no adapter is returned.

## audio

### `audio`

- **Reads**: an `OfflineAudioContext` (1 channel, 5000 frames at 44100 Hz) renders a 10 kHz triangle wave through a dynamics compressor; the last 500 samples are taken. Nothing is played and no audio device is opened.
- **Why it helps**: the floating-point details of the audio stack differ between engines, operating systems and CPUs.
- **Typical value**: digest.
- **Support**: all current browsers. `blocked` / `silent` when the render is all zeros; `excluded` under Brave.

## fonts

Which fonts are installed, and how the default fonts are set, depend on the operating system, its language packs and installed software.

### `fonts`

- **Reads**: for each of 54 font families, text in that font with a generic fallback (`monospace`, `sans-serif`, `serif`) is measured in hidden spans and compared with the fallback alone. A family counts as present when any width or height differs. The spans sit in one hidden, off-screen container that is removed at once.
- **Why it helps**: installed fonts vary a lot between systems.
- **Typical value**: digest of the sorted list of found families, or `none`.
- **Support**: needs a `document.body` and layout (`unavailable` / `no_document` or `no_layout` otherwise). `excluded` under Brave.

### `fonts_canvas`

- **Reads**: the same 54 families, detected a second way: canvas `measureText()` widths at 72 px against the three fallbacks. No DOM layout, so page CSS cannot disturb it.
- **Typical value**: digest, or `none`.
- **Support**: all browsers with a 2D canvas. `excluded` under Brave.

### `font_preferences`

- **Reads**: the width of the text “Whiskers quietly judge 0123456789” in seven hidden spans: the browser default font, `serif`, `sans-serif`, `monospace`, `system-ui`, `-apple-system`, and the default font at 1 px (which reveals the minimum font size setting).
- **Why it helps**: follows the user’s font settings and the operating system’s system font.
- **Typical value**: digest.
- **Support**: needs a body and layout.

## device: screen, memory and CPU

### `screen_resolution`

- **Reads**: `screen.width` and `screen.height`, larger side first, so that rotating a phone does not change it.
- **Typical value**: `1920x1080`, `2560x1440`, `932x430`.
- **Support**: all browsers. `excluded` under Firefox resistFingerprinting.

### `color_depth`

- **Reads**: `screen.colorDepth`.
- **Typical value**: `24`, `30`.

### `hardware_concurrency`

- **Reads**: `navigator.hardwareConcurrency` (logical CPU threads).
- **Typical value**: `8`, `12`.
- **Support**: all current browsers. Some browsers cap or round it. `excluded` under Brave and Firefox resistFingerprinting.

### `device_memory`

- **Reads**: `navigator.deviceMemory` (approximate GB, coarse buckets).
- **Typical value**: `8`, `4`.
- **Support**: Chromium-based browsers only. Safari, iOS browsers and Firefox do not offer it, so it is `unavailable` / `no_api` there by design.

### `heap_limit`

- **Reads**: `performance.memory.jsHeapSizeLimit`.
- **Typical value**: `4294705152`.
- **Support**: Chromium-based browsers only; `unavailable` elsewhere.

### `cpu_architecture`

- **Reads**: computes `Infinity - Infinity` in a `Float32Array` and reports the high byte of the resulting NaN’s bit pattern.
- **Why it helps**: x86 produces a NaN with the sign bit set, ARM one without, so it tells CPU families apart even when the user agent hides them.
- **Typical value**: `255` (x86), `127` (ARM).

### `touch_support`

- **Reads**: `navigator.maxTouchPoints`, whether `document.createEvent('TouchEvent')` works, and whether `ontouchstart` exists on the window.
- **Typical value**: `points=0,event=0,start=0` (desktop), `points=5,event=1,start=1` (phone).

## browser: engine and build

### `browser_class`

- **Reads**: WhiskerJS’s own class: `brave`, `firefox_rfp`, `firefox`, `safari`, `chromium` or `other`, from `navigator.brave`, the user agent, `navigator.vendor` and the resistFingerprinting traits.
- **Why it helps**: picks the exclusion row; separates browser families.
- **Typical value**: `chromium`.

### `math`

- **Reads**: 24 Math results (acos, acosh, asin, asinh, atanh, atan, sin, sinh, cos, cosh, tan, tanh, exp, expm1, log1p and polyfill forms of some, plus `PI` to the power -100) on fixed inputs. The operations and inputs are adapted from FingerprintJS 5.3.0 (MIT).
- **Why it helps**: the last digits of these functions differ between JavaScript engines and their versions.
- **Typical value**: digest.

### `media_codecs`

- **Reads**: for 8 audio and 12 video MIME types, `canPlayType()` on a new `<audio>` or `<video>` element (never given a source, never attached) and `MediaSource.isTypeSupported()`. The MIME lists are adapted from fpscanner 1.0.8 (MIT).
- **Typical value**: digest.

### `plugins`

- **Reads**: `navigator.plugins`: each plugin’s name, file name and MIME types, sorted.
- **Why it helps**: mostly a fixed PDF plugin list in current browsers, but it differs between engines and some embedded browsers.
- **Typical value**: digest, or `none`.
- **Support**: `excluded` under Brave.

### `pdf_viewer`

- **Reads**: `navigator.pdfViewerEnabled`.
- **Typical value**: `true` or `false`; `unavailable` where the property does not exist.

### `platform`

- **Reads**: `navigator.platform`.
- **Typical value**: `MacIntel`, `Win32`, `Linux x86_64`, `iPhone`.

### `vendor`

- **Reads**: `navigator.vendor`, with an empty string reported as `empty`.
- **Typical value**: `Google Inc.`, `Apple Computer, Inc.`, `empty` (Firefox).

### `vendor_flavors`

- **Reads**: which of ten browser-specific globals exist (`chrome`, `safari`, `webkit`, `opr`, `yandex`, `__crWeb`, `__gCrWeb`, `__firefox__`, `samsungAr`, `ucweb`).
- **Why it helps**: tells browser builds and in-app browsers apart.
- **Typical value**: `chrome`, or `none`.

### `storage`

- **Reads**: whether `localStorage`, `sessionStorage` and `indexedDB` are reachable (the property is only looked up, never read or written) and `navigator.cookieEnabled`. No cookie is read or set.
- **Typical value**: `local=1,session=1,indexed_db=1,cookies=1`.

### `ua_client_hints`

- **Reads**: `navigator.userAgentData`: brand names (without versions, with the deliberately changing “GREASE” brand removed), `mobile`, `platform`, and from one `getHighEntropyValues()` call: architecture, bitness, wow64, model and form factors.
- **Why it helps**: the device class and architecture, more reliably than the user agent string.
- **Typical value**: `brands=Chromium,Google Chrome;mobile=0;platform=macOS;arch=arm;bitness=64;wow64=0;model=;form=Desktop`. When the detailed hints are refused by a Permissions Policy, the end is the stable marker `he=blocked` (or `he=failed`, `he=none`).
- **Support**: Chromium-based browsers; `unavailable` elsewhere.

### `apple_pay`

- **Reads**: whether `ApplePaySession` exists, the highest version from 20 down that `supportsVersion()` accepts, and `canMakePayments()`. No payment sheet is shown and no request is made.
- **Typical value**: `none` (no Apple Pay), `version=14;can_pay=1`. `can_pay` is `blocked` where the call throws (insecure or cross-origin contexts).
- **Support**: Safari; `none` elsewhere.

### `private_click`

- **Reads**: `attributionSourceId` of a new `<a>` element that gets no `href` and is never attached (Safari Private Click Measurement).
- **Typical value**: `none` outside Safari.

## locale

### `time_zone`

- **Reads**: the IANA name from `Intl.DateTimeFormat().resolvedOptions()`, or, without Intl, `UTC+n` from the larger of the January and July offsets.
- **Typical value**: `Europe/Berlin`, `America/New_York`.
- **Support**: `excluded` under Firefox resistFingerprinting.

### `languages`

- **Reads**: `navigator.languages` joined with commas, or `navigator.language`.
- **Typical value**: `en-US,en`.

### `intl_locale`

- **Reads**: the resolved locale, calendar and numbering system of `Intl.DateTimeFormat`, its hour cycle, and the locale of `Intl.NumberFormat`.
- **Typical value**: `locale=en-US;calendar=gregory;numbering=latn;hour_cycle=h12;number_locale=en-US`.

## prefs: display and accessibility preferences

Each is one `matchMedia()` query list; the first match is reported, and `unavailable` / `no_match` when none matches.

| Component | Values |
| --- | --- |
| [`color_gamut`](https://whiskerjs.com/signals/color-gamut/) | `rec2020`, `p3`, `srgb` |
| [`dynamic_range`](https://whiskerjs.com/signals/dynamic-range/) | `high`, `standard` |
| [`forced_colors`](https://whiskerjs.com/signals/forced-colors/) | `active`, `none` |
| [`prefers_contrast`](https://whiskerjs.com/signals/prefers-contrast/) | `more`, `less`, `custom`, `none` |
| [`reduced_motion`](https://whiskerjs.com/signals/reduced-motion/) | `reduce`, `none` |

They follow the screen and the user’s accessibility settings, which rarely change.

## Not in either id

These are reported so your server can use them, but they change too easily to be part of an identifier.

### `pixel_ratio`

- **Reads**: `devicePixelRatio`, rounded to 3 decimals.
- **Typical value**: `1`, `2`, `1.25`.
- **Why not in the id**: it changes with page zoom. `excluded` under Firefox resistFingerprinting.

### `screen_frame`

- **Reads**: the screen area the system keeps (taskbar, dock, menu bar) as `top,right,bottom,left` in CSS pixels, from `screen` minus the `avail*` values.
- **Typical value**: `0,0,48,0` (a 48 px taskbar at the bottom).
- **Why not in the id**: it changes when the taskbar moves or with a second monitor. `excluded` under Firefox resistFingerprinting.

### `ua_versions`

- **Reads**: full brand versions and the platform version from Client Hints.
- **Typical value**: `brands=Chromium/130.0.6723.58,Google Chrome/130.0.6723.58;platform_version=15.1.0`.
- **Why not in the id**: it changes with every browser or OS update. Chromium-based browsers only.

### `permissions`

- **Reads**: the state (`granted`, `denied`, `prompt`; `unsupported` or `unknown` otherwise) of 24 permissions through `navigator.permissions.query()`, which never prompts. No API that requests a permission is ever called.
- **Typical value**: digest.
- **Why not in the id**: it changes when the user grants or revokes a permission. The notifications state also feeds the `permissions_inconsistent` flag.

### `keyboard_layout`

- **Reads**: the character 19 physical keys produce in the active layout, from `navigator.keyboard.getLayoutMap()` (no prompt); `_` for a key the layout lacks.
- **Typical value**: `` qwyzam;'[]`\\-=,./2 `` (US English); hashed for layouts outside printable ASCII.
- **Why not in the id**: users switch layouts. Chromium only; `blocked` in cross-origin iframes, where the browser refuses it.

### `speech_voices`

- **Reads**: installed speech synthesis voices (name, language, local, default), sorted. Waits up to 600 ms for the `voiceschanged` event if the list is empty at first. No speech is produced.
- **Typical value**: digest, or `none`.
- **Why not in the id**: some browsers fill the list late.

### `audio_latency`

- **Reads**: `AudioContext.baseLatency`, and closes the context at once. Nothing is played.
- **Typical value**: a number of seconds such as `0.005333`.
- **Support**: read only in Safari and on Android. Elsewhere creating an `AudioContext` outside a user gesture prints an autoplay warning in the console, so it reports `unavailable` with ruleId `skipped`.
- **Why not in the id**: it follows the audio output device.

### `canvas_readback`

- **Reads**: paints two known colours into a 1x1 canvas and reads them back with `getImageData()`.
- **Typical value**: `exact`, or `altered` when something adds noise to canvas reads.
- **Why not in the id**: it is a diagnostic about the browser’s settings, not about the device.

### `content_blockers`

- **Reads**: adds eight 1x1 bait elements with ad-like class names and ids (for example `ad-banner`, `adsbox`) in an off-screen container, waits 50 ms, and lists the baits that cosmetic filters hid. Everything is removed afterwards; no request is made.
- **Typical value**: `none`, or `ad_banner,ad_box,banner_ad`.
- **Why not in the id**: users turn blockers on and off per site.

## Left out on purpose

WebRTC (even without addresses), media device lists, network information, battery, geolocation, storage quota and user-agent parsing are not collected. The reasons are in the roadmap in the library source (`docs/ROADMAP.md`).
