Skip to content
Anti-Bot

What Is DOMRect Fingerprinting?

Pim

Pim · Scrappey Research

July 30, 2026 4 min read

On this page

DOMRect fingerprinting measures the exact sub-pixel geometry the layout engine assigns to elements, usually through getBoundingClientRect(). The returned coordinates are floating-point numbers produced by text shaping, font rasterisation, and layout arithmetic, so an element sized by its text content lands on values that vary with the installed fonts, the rendering backend, and the operating system. It is closely related to font fingerprinting but reads the layout box rather than the glyph metrics.

Quick facts

Read throughgetBoundingClientRect(), getClientRects(), Range rects, IntersectionObserver
Why values varyText shaping, font rasterisation, and layout arithmetic differ by platform
PrecisionSub-pixel floating point - the fractional part carries the signal
Perturbation targetSome privacy browsers add small offsets to returned rects
Why noise strugglesRect values are arithmetically related to each other and to computed style

Where the entropy comes from

Set an element to size itself to a string of text and measure it. The width is not a round number: it is the sum of the advance widths of every glyph, computed after font selection, hinting, and sub-pixel positioning. Change the font file, the rasteriser, or the platform text-shaping library and the fractional part of that sum changes with it.

This gives DOMRect measurements a useful property from a detection standpoint - they are a proxy for the font and rendering stack that does not require enumerating fonts directly. A probe can render a handful of strings in a handful of font stacks, read the resulting rects, and hash the fractional components. The result correlates strongly with the operating system and its installed font set without ever asking which fonts are present.

Several other APIs expose the same underlying geometry through different objects: getClientRects() returns the individual boxes of a fragmented inline element, a Range over the same text returns its own rects, IntersectionObserver reports a bounding rect for an observed element, and SVG exposes a screen coordinate matrix. All of them are computed from one layout, which is what makes the surface interesting for coherence work as well as for entropy.

Why perturbation is unusually hard here

Because rect values are arithmetically related to one another, small random offsets added to defeat hashing create contradictions rather than noise. The relationships a probe can test are simple and exact:

  • Bounding rect against Range rects - the bounding box of an element must be consistent with the rects of the text inside it.
  • Layout box arithmetic - clientWidth, offsetWidth, and the computed style width, padding, and border are related by a documented formula. The rect must satisfy it.
  • Origin anchoring - an element positioned at the document origin must measure its own origin as zero. A perturbed rect puts it near zero instead.
  • Observer agreement - the rect reported by IntersectionObserver describes the same element as getBoundingClientRect() and must match.
  • Advance additivity - a string of N identical glyphs must measure N times the width of one, within rounding. Independent noise per measurement breaks the proportion.

This is the same lesson that appears in canvas and WebGL readback: the check is not "is this value rare" but "is this value arithmetically possible". Uniform per-session offsets applied consistently to every geometry API can preserve the relationships, but doing so requires the perturbation to live below the layout engine rather than in front of the JavaScript API - which is the difference between an engine-level implementation and a patched accessor that integrity checks can see directly.

How much it actually matters

DOMRect entropy is real but modest, and it overlaps heavily with signals a probe is already collecting. If font fingerprinting, canvas text metrics, and platform oracles are all being read, the incremental information from layout geometry is small - it mostly confirms what those already said.

Its practical importance is therefore as a consistency check rather than an identifier, and as a trap for perturbation applied at the wrong layer. A configuration that returns clean, self-consistent geometry matching its declared platform passes without the surface ever mattering. One that adds noise to getBoundingClientRect alone announces that something is intercepting measurement, which is a stronger signal than the layout hash it was trying to suppress.

The straightforward path is to render honestly on a real platform with a normal font set, and let the geometry be whatever the layout engine computes. Managed browser infrastructure does this by default, so the relationships hold without any perturbation strategy to get right.

Code example

javascript
// DOMRect: the measurement, and the relationships noise has to preserve.
const el = document.createElement('span');
el.style.cssText = 'position:absolute; top:0; left:0; font:16px serif; white-space:pre';
el.textContent = 'mmmmmmmmmmlli';
document.body.appendChild(el);

const rect = el.getBoundingClientRect();

// 1. Entropy: the fractional part carries the font/rasteriser signal
const signature = [rect.width, rect.height].map(n => n.toFixed(10)).join('|');

// 2. Origin anchoring - an element at 0,0 must measure exactly zero
const anchored = rect.left === 0 && rect.top === 0;

// 3. Bounding rect must agree with a Range over the same content
const range = document.createRange();
range.selectNodeContents(el);
const rangeRect = range.getBoundingClientRect();
const rangeAgrees = Math.abs(rangeRect.width - rect.width) < 0.01;

// 4. Advance additivity - N glyphs measure N times one glyph
const one = document.createElement('span');
one.style.cssText = el.style.cssText;
one.textContent = 'm';
document.body.appendChild(one);
const oneW = one.getBoundingClientRect().width;

const ten = document.createElement('span');
ten.style.cssText = el.style.cssText;
ten.textContent = 'mmmmmmmmmm';
document.body.appendChild(ten);
const additive = Math.abs(ten.getBoundingClientRect().width - oneW * 10) < 0.05;

console.log({ signature, anchored, rangeAgrees, additive });
[el, one, ten].forEach(n => n.remove());

Related terms

Concept map

Concept map

How DOMRect Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Why does getBoundingClientRect return fractional pixel values?

Because layout is computed in sub-pixel precision before being painted. An element sized by its text content gets a width equal to the sum of glyph advance widths after font selection, hinting, and shaping, and that sum is almost never a whole number. The fractional component is exactly where the platform-specific signal lives.

Does adding random noise to DOMRect values protect against fingerprinting?

Not reliably, because rect values are arithmetically related to each other and to computed style. Independent noise per measurement breaks additivity across repeated glyphs, makes an element positioned at the origin measure a non-zero origin, and causes the bounding rect to disagree with Range rects for the same content. Those contradictions are easier to detect than the layout hash the noise was meant to suppress.

Is DOMRect fingerprinting worth worrying about on its own?

Rarely. Its entropy is modest and overlaps heavily with font and canvas text metrics, so a probe collecting those learns little extra from layout geometry. It matters mainly as a consistency check and as a place where perturbation applied at the wrong layer becomes visible.

Last updated: 2026-07-30 · Facts last verified: 2026-07-30