CSS @supports Fingerprinting: How CSS Capability Probes Reveal Your Engine Build
CSS feature detection is a fingerprinting vector that gets little attention. It is not as visible as canvas or WebGL, and there is no image hash to show off. It is still effective, because it tests what your browser actually is, not what it reports. Changing a User-Agent string takes one header. Changing what the rendering engine supports requires a different engine.
This tutorial explains how CSS.supports() probing works, which features scripts use as version markers, and how to audit a team's browser profiles so the engine, the User-Agent and the Client Hints all agree. It is written for site owners who want to understand what their visitors reveal, QA engineers testing across browser versions, and teams that keep many profiles consistent.
Check your own fingerprint first. We built a public checker at justbrowser.app/fingerprint-checker that runs client-side, with no signup. It grades what it claims rather than guessing: WebRTC candidates are compared against your public IP, and the IP's location is graded on whether its UTC offset actually agrees with your browser's timezone. Worth running before you read the rest of this, so the signals below have your numbers attached to them.
What We're Building
By the end of this, you'll understand:
- How CSS.supports() and @supports expose engine capabilities
- Which CSS properties are used as version markers
- Why a changed User-Agent without a matching engine produces an inconsistent browser
- How to audit your profiles for CSS inconsistencies
- What a native engine means in practice
You'll need a browser profile to test, in JustBrowser, Multilogin, GoLogin or whichever multi-account browser you run. We are going to write probe code and see what it reports.
Prerequisites
- A multi-account browser with configurable User-Agent
- Basic JavaScript knowledge (enough to run code in DevTools)
- Understanding of browser version numbers (Chrome 125, Firefox 128 and so on)
- 20 minutes
For background on fingerprinting fundamentals, the WebGL fingerprinting deep-dive covers concepts that apply here too. For how audio, font and hardware signals combine with CSS capabilities, see the 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', 'color-mix(in srgb, red 50%, blue)')); // depends on version
That last one is the interesting case. color-mix() shipped in Chrome 111 (March 2023). If your browser returns true, you are on Chrome 111 or later. If false, you are older.
Test enough features and you can narrow down the 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: '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 a profile and you get a pattern of true and false values that maps to a Chromium release. (At-rules such as @starting-style can't be tested this way: CSS.supports() takes declarations and selector() tests, so scripts stick to properties, values and selectors.) If anchor-name returns false while light-dark returns true, the engine sits between Chrome 123 and 124. That is narrow enough for most version-identification purposes, and it takes a few lines.
Step 2: The Version Boundary Problem
Here is where a mismatch appears. Suppose a profile's User-Agent reports a newer Chrome than the engine actually is. The string changed, the browser did not. A script can compare the two:
// 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: User-Agent and engine disagree');
}
Sites do not publish their logic, but a mismatch between the reported version and real capabilities is an easy inconsistency for any script to check, and it makes test and QA results unrepresentative of the browser you meant to test.
This is not sophisticated analysis. The mismatch exists because many tools do not touch the engine. They wrap a stock Chromium build, which is sometimes well behind upstream, and apply identity changes through extensions or JavaScript. That wrapper is transparent to CSS.supports().
Step 3: Building a CSS Capability Probe
Here is a fuller probe you can run against your own profiles. It is roughly what a site-side script does.
function getCSSFingerprint() {
const probes = [
// Chromium version markers (first Chrome release with support)
{ name: 'container-queries', test: 'container-type: inline-size', chrome: 105 },
{ name: 'has-selector', test: 'selector(:has(*))', chrome: 105 },
{ name: 'color-mix', test: 'color: color-mix(in srgb, red, blue)', chrome: 111 },
{ name: 'nesting', test: 'selector(&)', chrome: 112 },
{ name: 'linear-timing', test: 'animation-timing-function: linear(0, 1)', chrome: 113 },
{ name: 'light-dark', test: 'color: light-dark(white, black)', chrome: 123 },
{ name: 'field-sizing', test: 'field-sizing: content', chrome: 123 },
{ name: 'anchor-name', test: 'anchor-name: --a', chrome: 125 },
{ name: 'interpolate-size', test: 'interpolate-size: allow-keywords', chrome: 129 },
// Cross-engine marker: Firefox supported this long before Chrome
{ name: 'scrollbar-width', test: 'scrollbar-width: thin', chrome: 121 },
];
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 it on your profiles and compare the estimated version to the User-Agent. If they do not match, you have an inconsistency to fix.
The hash at the end is the fingerprint itself: two browsers with identical CSS capabilities produce identical hashes. Profiles that report different browsers but share a hash are running the same engine.
Step 4: Where Extension-Based Tools Fall Short
Extension-based tools work by injecting JavaScript that overrides browser APIs. They can change what navigator.userAgent returns, and they can hook canvas rendering. They cannot change what CSS.supports() returns, because that queries the rendering engine directly.
There is no JavaScript hook for "support a CSS property the engine does not support". The property either parses or it does not, and that parsing happens in Blink, Chromium's renderer, not in script.
This is the same reason checkers such as CreepJS can spot overridden values, and the same reason a TLS fingerprint can't be changed from page script: you can override values in the main thread, but the engine's behavior is what it is. For automation, the Playwright and Puppeteer REST API guide covers how a native engine behaves under scripted control.
When a script sees:
- User-Agent: a recent Chrome release
- CSS capabilities: an older release
- Client Hints: a recent Chrome release
it concludes the recent-version signals are inconsistent with the older ones. The inconsistency itself says more than any individual data point.
Step 5: What a Native Engine Means
JustBrowser ships a custom Chromium engine with native C++ integration: not a wrapper around an older build and not an extension injecting into stock Chrome, but Chromium with identity parameters patched at the source level.
What this means for CSS capabilities:
-
CSS capabilities match the reported version. A profile reports the version the engine actually is, and the engine is patched on every upstream Chromium release. The User-Agent, Client Hints and
CSS.supports()tell the same story. -
No override layer. The CSS parsing is genuine, so a capability probe simply reports the real engine.
-
New features arrive with the engine. When upstream Chrome ships new CSS properties, the patched engine picks them up as part of the same release.
The comparison page describes how this differs architecturally from Multilogin, GoLogin and AdsPower. We have also published related comparisons: myths about multi-account browsers and the ad verification workflow. Some tools use native engines, some use extension injection, and price does not always correlate with architecture.
If you need your CSS capabilities to match what a profile reports, you need an engine that really is that version. There is no script-level substitute.
Step 6: Auditing Your Profiles
Here is a practical workflow.
1. Check the version a profile reports.
Run this in DevTools:
const match = navigator.userAgent.match(/Chrome\/(\d+)/);
console.log('Reported:', match ? match[1] : 'Unknown');
2. Check its CSS capabilities.
Run the probe from Step 3 and note the estimated version.
3. Compare.
If the reported version is higher than the estimate, the profile is reporting a newer engine than it has. If lower, it is unusual but can happen with a development build. Either way the mismatch is readable by any site.
4. Cross-check with public tools.
CreepJS includes some CSS probing, and so does FingerprintJS. Run both and look at the consistency notes.
5. Compare across profiles.
Profiles from the same app on the same engine share a CSS hash. That is expected. For teams, the relevant point is that CSS capabilities will not distinguish one profile from another, so each profile's cookies, storage and proxy are what keep client or test accounts separate.
Common Errors and Fixes
Error: "Reported Chrome version is newer than the CSS capabilities show"
Cause: The tool wraps an outdated Chromium build and edits the User-Agent.
Fix: Use a multi-account browser with a current native engine. JustBrowser is patched on every upstream Chromium release, so the engine does not trail the version a profile reports. If you use another tool, check its engine version documentation, since many do not publish it.
Error: "Firefox-first features detected on a Chrome profile"
Cause: Some CSS properties ship in Firefox before Chrome, or the other way round. A profile showing Firefox-first features while reporting Chrome points to a mixed engine.
Fix: Verify the tool is actually Chromium-based. This is rare.
Error: "CSS fingerprint identical across supposedly different profiles"
Cause: All profiles share the same base engine, which is expected.
Fix: This is normal for profiles in the same app. The issue to look for is a CSS fingerprint that differs from the reported identity, so focus on making capabilities match the User-Agent rather than varying CSS between profiles.
Error: "Capabilities flip between tests"
Cause: Some tools randomize fingerprint values, including CSS tests.
Fix: Turn off randomization. CSS capabilities should be stable and appropriate to the version. Randomizing CSS.supports() results creates impossible browser states.
The Bigger Picture
CSS feature detection is one signal among many. A profile that is consistent on CSS but inconsistent on WebGL, canvas or audio is still an incoherent browser.
The reason to look at CSS specifically is that it is easy to overlook. People focus on canvas hashes because they are visible and concrete, while style capabilities are invisible booleans. CSS.supports() is still a direct truth test of what the engine supports.
For teams running ad verification or QA across versions, the same logic helps: knowing which engine you are really running tells you whether your test results represent the browser you intended. Our ad verification workflow guide covers a full setup. Pair it with ClickzProtect for click-traffic monitoring and JustAnalytics for privacy-respecting conversion tracking.
Next Steps
Once your profiles are consistent on CSS:
- Look at the rest of the picture. A checker such as CreepJS shows many more signals than CSS.
- Check other version-specific APIs, such as
navigator.userAgentData(Client Hints) andperformance.measureUserAgentSpecificMemory(). - Document your reference configuration. When something works, save it.
- Re-check after engine updates. New Chrome releases mean new feature probes.
The profiles that give representative test results are the ones where every signal tells the same story. CSS capabilities, WebGL parameters, canvas rendering, audio output and Client Hints should all agree on which browser is running.
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(), a script knows you are running Chromium 111-122. Each feature boundary narrows the version range until the major version is clear.
Is CSS feature detection more reliable than User-Agent parsing?
Yes. User-Agent strings can be changed with a single header, but CSS.supports() queries the actual rendering engine. A browser can claim to be a newer Chrome, but if its engine lacks features that version has, the mismatch is visible. Feature detection tests capabilities, not claims.
Can multi-account browsers change CSS feature support?
Extension-based tools cannot, since they inject JavaScript but do not modify the rendering engine. JustBrowser's Chromium engine is patched on every upstream Chromium release, so the CSS capabilities it exposes are exactly the ones its own core ships. That is the part that matters for CSS.supports(): the profile reports the core it actually is, so capability probes and the reported version agree.
Which CSS features are most commonly used for fingerprinting?
Scripts probe recently shipped features with clear version boundaries: color-mix() (Chrome 111), light-dark() (Chrome 123), field-sizing (Chrome 123), anchor positioning (Chrome 125), scrollbar-width (Firefox/Chrome divergence), and the :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
A multi-account browser for teams, built on a custom Chromium engine with native C++ integration, not an extension. Each profile is its own browser with its own cookies, storage, proxy and 40+ identity parameters, and profiles can be shared with teammates. Local REST API and CDP 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.