JustBrowser
Tutorials13 min read

CSS @supports Fingerprinting: How Detection Scripts Expose Your Engine

JustBrowser Platform Team·
css-fingerprintingbrowser-fingerprintingcss-supports-apiantidetect-browserchromium-detectionbuildinpublicsaasstudioaiworkforcebuildwithclaude

Last week I was auditing a client's antidetect setup — 30 profiles, different proxies, different canvas hashes, different WebGL renderers. On paper, solid separation. Then I ran a custom detection script that just tested CSS feature support.

Every single profile reported identical results. Same CSS capabilities. Same engine. One fingerprint. I felt like an idiot for not checking this sooner.

They'd spoofed everything except the one thing that actually matters: the browser itself. Classic mistake. I've made it too.

CSS feature detection is the fingerprinting vector nobody talks about. It's not as flashy as canvas or WebGL. There's no visual hash to show off. But it's devastatingly effective because it tests what your browser actually is, not what it claims to be. And unlike User-Agent spoofing — which any script kiddie can pull off — faking CSS capabilities requires modifying the rendering engine. Extensions can't do it. JavaScript overrides can't do it. You either have the right engine, or you don't.

This tutorial breaks down how CSS.supports() fingerprinting works, which features detection scripts probe, and how to configure profiles that don't leak version mismatches.

What We're Building

By the end of this, you'll understand:

  1. How CSS.supports() and @supports expose engine capabilities
  2. Which CSS properties are used as version markers
  3. Why User-Agent spoofing without engine alignment fails
  4. How to audit your profiles for CSS fingerprint leaks
  5. What native engine antidetect actually means in practice

You'll need a browser profile to test — JustBrowser, Multilogin, GoLogin, whatever you're running. We're going to write detection code and see what it reveals.

Prerequisites

  • An antidetect browser with configurable User-Agent
  • Basic JavaScript knowledge (enough to run code in DevTools)
  • Understanding of browser version numbers (Chrome 125, Firefox 128, etc.)
  • 20 minutes

If you need background on fingerprinting fundamentals, the WebGL fingerprinting deep-dive covers detection concepts that apply here too. For a broader look at how audio, font, and hardware signals combine with CSS fingerprinting, see our overlooked fingerprinting vectors guide.

Step 1: Understanding CSS.supports()

The CSS.supports() API lets JavaScript test whether the browser supports a CSS property-value pair. Open DevTools and run:

console.log(CSS.supports('color', 'red'));  // true — every browser
console.log(CSS.supports('display', 'grid'));  // true since ~2017
console.log(CSS.supports('color-mix(in srgb, red 50%, blue)'));  // depends on version

That last one is interesting. color-mix() shipped in Chrome 111 (March 2023). If your browser returns true, you're on Chrome 111+. If false, you're older.

Now here's the fingerprinting insight: test enough features, and you can pinpoint the exact version.

const features = [
  { name: 'color-mix', test: 'color: color-mix(in srgb, red, blue)', since: 111 },
  { name: 'light-dark', test: 'color: light-dark(white, black)', since: 123 },
  { name: '@starting-style', test: '@starting-style {}', since: 117 },
  { name: 'anchor-name', test: 'anchor-name: --foo', since: 125 },
  { name: 'field-sizing', test: 'field-sizing: content', since: 123 },
];

features.forEach(f => {
  console.log(`${f.name} (Chrome ${f.since}+):`, CSS.supports(f.test));
});

Run that in your antidetect profile. You'll get a pattern of true/false that maps directly to your Chromium build. If anchor-name returns false but light-dark returns true, you're between Chrome 123 and 124. Narrow enough for most detection purposes.

Honestly, it's almost too easy. And that's the frustrating part — this stuff has been documented for years, but I still see people spending hours on canvas spoofing while ignoring a five-line CSS probe.

Step 2: The Version Boundary Problem

Here's where antidetect browsers fall apart.

Say you configure a profile with User-Agent claiming Chrome 146 (the current stable). But your actual browser engine is Chromium 120. You've changed the string, not the browser. Detection scripts run:

// Chrome 125+ supports anchor positioning
const claimedVersion = parseInt(navigator.userAgent.match(/Chrome\/(\d+)/)?.[1]);
const hasAnchorPositioning = CSS.supports('anchor-name', '--test');

if (claimedVersion >= 125 && !hasAnchorPositioning) {
  console.log('Version mismatch detected — spoofed UA');
}

That's it. Game over. Your profile claims 146, but the engine can't back it up.

I've seen this exact pattern on platforms like Amazon and Facebook. They don't publish their detection logic, but the account suspensions spike when version claims don't match capabilities. The correlation is too consistent to ignore.

This isn't sophisticated detection. Any junior dev could write this script in 20 minutes. The reason it works so well? Most antidetect tools don't touch the engine. They wrap stock Chromium (sometimes hilariously outdated — I've seen tools shipping Chromium 108 in 2026) and inject spoofing via extensions or JavaScript. The wrapper is transparent to CSS.supports().

Step 3: Building a CSS Fingerprint Detector

Let's build a proper detection script to audit your profiles. This is what the other side is running (more or less).

function getCSSFingerprint() {
  const probes = [
    // Chromium version markers
    { name: 'container-queries', test: 'container-type: inline-size', chrome: 105, firefox: 110 },
    { name: 'has-selector', test: 'selector(:has(*))', chrome: 105, firefox: 121 },
    { name: 'color-mix', test: 'color: color-mix(in srgb, red, blue)', chrome: 111, firefox: 113 },
    { name: 'nesting', test: 'selector(&)', chrome: 112, firefox: 117 },
    { name: '@starting-style', test: '@starting-style {}', chrome: 117, firefox: null },
    { name: 'linear-timing', test: 'animation-timing-function: linear(0, 1)', chrome: 113, firefox: 112 },
    { name: 'light-dark', test: 'color: light-dark(white, black)', chrome: 123, firefox: 120 },
    { name: 'field-sizing', test: 'field-sizing: content', chrome: 123, firefox: null },
    { name: 'anchor-name', test: 'anchor-name: --a', chrome: 125, firefox: null },
    { name: 'interpolate-size', test: 'interpolate-size: allow-keywords', chrome: 129, firefox: null },
    // Firefox-only (absence in Chrome is a signal)
    { name: 'scrollbar-width', test: 'scrollbar-width: thin', chrome: 121, firefox: 64 },
  ];

  const results = {};
  probes.forEach(p => {
    results[p.name] = CSS.supports(p.test);
  });

  // Estimate version from results
  let estimatedChrome = 100;
  probes.filter(p => p.chrome).forEach(p => {
    if (results[p.name] && p.chrome > estimatedChrome) {
      estimatedChrome = p.chrome;
    }
  });

  return {
    capabilities: results,
    estimatedChromeVersion: estimatedChrome,
    hash: btoa(JSON.stringify(results)).slice(0, 16)
  };
}

console.log(getCSSFingerprint());

Run this on your profiles. Compare the estimated version to your User-Agent claim. If they don't match, you've got a leak.

The hash at the end is the fingerprint itself — two browsers with identical CSS capabilities produce identical hashes. Two profiles claiming to be different browsers but sharing a hash are obviously the same engine.

Step 4: Where Extension-Based Antidetect Fails

I need to be direct here because I see this confusion constantly, and it drives me up the wall.

Extension-based antidetect tools — and most of the budget tools fall in this category — work by injecting JavaScript that overrides browser APIs. They can change what navigator.userAgent returns. They can hook into canvas rendering. But they cannot change what CSS.supports() returns because that queries the rendering engine directly. It's physics, not policy.

There's no JavaScript hook for "make the browser support a CSS property it doesn't support." The property either parses or it doesn't, and that parsing happens in Blink (Chromium's renderer), not in script.

This is the same fundamental limitation that causes CreepJS lies detection — you can override values in the main thread, but the actual engine behavior is what it is. If you're running automation, our Playwright/Puppeteer stealth guide covers how native engines handle these detection challenges at scale.

When detection services see:

  • User-Agent: Chrome 146
  • CSS capabilities: Chrome 120
  • WebGL: Chrome 120 shader precision
  • Client Hints: Chrome 146

...they know the 146 claims are spoofed. The 120 signals are the truth. And the inconsistency itself is a stronger signal than any individual data point.

Step 5: Native Engine Antidetect — Why It Matters

Native antidetect browsers modify Chromium's C++ source code before compilation. JustBrowser ships a native Chromium engine — not a wrapper around an older build, not an extension injecting into stock Chrome, but actual Chromium with identity parameters patched at the source level.

What this means for CSS fingerprinting:

  1. CSS capabilities match claims. Profiles claim the version the engine actually is, and the engine is rebuilt as upstream Chromium moves — so the UA, Client Hints and CSS.supports() all tell the same story. No version mismatches.

  2. No detectable modification patterns. The CSS parsing is genuine — there's nothing to detect because nothing is being faked.

  3. Future-proofed against new features. When upstream Chrome ships new CSS properties, the rebased engine picks up those capabilities automatically.

The comparison page breaks down how this differs from Multilogin, GoLogin, and AdsPower architecturally. We've also published detailed comparisons: JustBrowser vs Multilogin and JustBrowser vs GoLogin. Some use native engines (Multilogin's MLA2, for instance), some use extension injection. Price doesn't always correlate with architecture. I wish it did — would make buying decisions easier.

For CSS fingerprint evasion specifically, you need native. Full stop. There's no workaround, no clever hack, no "one weird trick." Either your engine matches your claims or it doesn't.

Step 6: Auditing Your Profiles

Here's the practical workflow.

1. Check your engine version.

Run this in DevTools:

const match = navigator.userAgent.match(/Chrome\/(\d+)/);
console.log('Claimed:', match ? match[1] : 'Unknown');

2. Check your CSS capabilities.

Run the fingerprint script from Step 3. Note the estimated version.

3. Compare.

If claimed version > estimated version: you're leaking engine age. If claimed version < estimated version: unusual, but might happen if you're running a dev build. Either way, the mismatch is detectable.

4. Cross-check with external tools.

CreepJS includes some CSS probing. So does FingerprintJS. Run both and look for inconsistency flags.

5. Test across profiles.

If multiple "separate" profiles share identical CSS fingerprint hashes, they're running the same engine. That's a linking signal even if everything else differs.

Common Errors and Fixes

Error: "Claimed Chrome 146 but CSS shows 120-range capabilities"

Cause: Your antidetect browser is wrapping an outdated Chromium build.

Fix: Use an antidetect browser with a current native engine. JustBrowser is rebased on every upstream Chromium release, so the engine never trails the version a profile claims. If you're on another tool, check their engine version documentation — many don't publish it, which is itself a red flag.

Error: "Firefox features detected on Chrome profile"

Cause: Some CSS properties ship in Firefox before Chrome (or vice versa). If your profile shows Firefox-first features as supported while claiming Chrome, the engine type is mixed.

Fix: Verify your antidetect browser is actually Chromium-based, not a Firefox fork claiming to be Chrome. This is rare but I've seen it with sketchy free tools.

Error: "CSS fingerprint identical across supposedly different profiles"

Cause: All profiles share the same base engine (expected) but you're not isolating other fingerprint vectors.

Fix: CSS fingerprint can be identical across profiles — that's fine if you're using the same antidetect browser. The issue is when CSS fingerprint differs from claimed identity. Focus on making the CSS capabilities match the User-Agent claim, not on varying CSS between profiles. I spent way too long trying to vary CSS fingerprints before realizing that was the wrong goal entirely.

Error: "Capabilities flip between tests"

Cause: Some antidetect tools randomize fingerprint values, including CSS tests.

Fix: Turn off randomization. CSS capabilities should be stable and version-appropriate, not randomized. Randomizing CSS.supports() results is worse than leaving them alone — it creates impossible browser states.

The Bigger Picture

CSS feature detection is one fingerprint vector among many. A profile that passes CSS checks but fails WebGL, canvas, or audio fingerprinting is still compromised.

The reason I'm highlighting CSS specifically? It's the most underrated vector I see. People obsess over canvas hashes (visible, hashable, feels concrete) while ignoring style capabilities (invisible, boolean, feels trivial). But CSS.supports() is a hard truth test. You can't lie about what your engine supports. Period.

For teams running ad verification or competitive intelligence, inconsistent CSS fingerprints mean your monitoring profiles are detectable. The sites you're checking know you're checking. That defeats the purpose. Our ad verification workflow guide covers the full setup. Pair your antidetect setup with ClickzProtect for click fraud monitoring and JustAnalytics for privacy-respecting conversion tracking — but those tools can't help if your browser identity is blown before you start.

Next Steps

Once your profiles pass CSS fingerprint audits:

  1. Run the full CreepJS suite — CSS is one piece, not the whole picture
  2. Verify other version-specific APIs — navigator.userAgentData (Client Hints), performance.measureUserAgentSpecificMemory(), etc.
  3. Document your passing configuration — when something works, save it
  4. Update when the engine updates — new Chrome releases mean new feature probes

The profiles that survive are the ones where every signal tells the same story. CSS capabilities, WebGL parameters, canvas rendering, audio fingerprint, Client Hints — they all need to agree on which browser you're running.

Get that right and detection scripts have nothing to grab. Get it wrong anywhere and the whole stack falls apart. I've debugged enough failed setups to know — CSS is usually where I start looking now.

Frequently Asked Questions

How does CSS.supports() reveal browser version information?

CSS.supports() returns true or false for any CSS property-value pair. Since CSS features ship with specific browser versions, testing a sequence of properties creates a fingerprint. If your browser supports color-mix() but not light-dark(), detection scripts know you're running Chromium 111-122. Each feature boundary narrows the version range until the exact build is identifiable.

Is CSS feature detection more reliable than User-Agent parsing?

Yes. User-Agent strings can be spoofed with a single header change, but CSS.supports() queries the actual rendering engine. You can claim to be Chrome 146, but if your engine doesn't support features Chrome 146 has, the lie is detectable. Feature detection tests capabilities, not claims.

Can antidetect browsers spoof CSS feature support?

Extension-based antidetect browsers cannot — they inject JavaScript but don't modify the rendering engine. Native antidetect browsers with C++ engine modifications can align CSS capabilities with the claimed version. JustBrowser's Chromium engine is rebuilt as upstream Chromium moves, so the CSS capabilities it exposes are exactly the ones its own core ships — the engine never claims a version it can't back up. That is the part that matters for CSS.supports(): the profile reports the core it actually is, so capability probes and the claimed version agree.

Which CSS features are most commonly used for fingerprinting?

Detection scripts probe recently-shipped features with clear version boundaries: color-mix() (Chrome 111), light-dark() (Chrome 123), @starting-style (Chrome 117), anchor positioning (Chrome 125), scrollbar-width (Firefox/Chrome divergence), and :has() pseudo-class (Chrome 105). The specific features matter less than the pattern — any property with a known ship date becomes a version marker.


Try JustBrowser

Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy