CSS Fingerprinting: How Stylesheets and Media Queries Track You
How CSS media queries like color-depth and prefers-color-scheme create fingerprint signals, and how to ensure consistent CSS identity.
Introduction
CSS is often overlooked as a fingerprinting vector because it is primarily a styling language. But CSS media queries expose information about the user's display, preferences, and browser capabilities. Properties like prefers-color-scheme, prefers-reduced-motion, color-gamut, resolution, and forced-colors reveal device configuration details that contribute to a fingerprint. What makes CSS-based fingerprinting particularly effective is that it works without JavaScript. A cleverly constructed stylesheet can extract device information through conditional resource loading, making it invisible to script-based privacy tools. This article explains how CSS signals contribute to fingerprinting and how BotBrowser ensures that CSS media queries, JavaScript matchMedia() calls, and related APIs all return consistent, profile-driven values.
Privacy Impact
CSS-based fingerprinting has gained attention in the privacy research community because it operates on a different layer than JavaScript fingerprinting. A 2021 study from TU Graz demonstrated that CSS media features alone could distinguish roughly 30% of users when 15+ media features were queried simultaneously. When combined with JavaScript-accessible properties (which should agree with CSS values), the identification rate increased significantly.
The W3C Media Queries Level 5 specification acknowledges the fingerprinting potential of media features. The specification notes that features like prefers-color-scheme, prefers-contrast, prefers-reduced-motion, and prefers-reduced-data each reveal a user preference that varies across the population. While each feature individually has low entropy (typically a boolean or a few possible values), the combination of many features creates a meaningful fingerprint.
CSS fingerprinting is particularly concerning because it can operate without JavaScript execution. A tracking pixel loaded conditionally via a @media rule tells the server about the user's display configuration through the request alone. This means that even users who block JavaScript entirely can be partially fingerprinted through CSS.
Technical Background
CSS Media Features Relevant to Fingerprinting
CSS media queries test conditions about the user's environment. The following features are most relevant to fingerprinting:
Display Properties:
resolution/min-resolution/max-resolution- Screen pixel density in dpi or dppx.color- Number of bits per color component.color-index- Number of colors in the device's color lookup table.color-gamut- Approximate color gamut:srgb,p3, orrec2020.monochrome- Whether the device is monochrome and bits per pixel.
User Preferences:
prefers-color-scheme- User's preferred color scheme:lightordark.prefers-reduced-motion- Whether the user has requested reduced motion.prefers-contrast- Whether the user has requested high or low contrast.prefers-reduced-transparency- Whether reduced transparency is preferred.forced-colors- Whether the browser is in forced colors mode (Windows High Contrast).
Viewport and Display:
width/height- Viewport dimensions.device-width/device-height- Screen dimensions (deprecated but still supported).aspect-ratio- Viewport aspect ratio.orientation- Viewport orientation:portraitorlandscape.
Interaction:
hover- Whether the primary pointing device can hover.any-hover- Whether any pointing device can hover.pointer- Precision of the primary pointing device:none,coarse, orfine.any-pointer- Precision of any available pointing device.
How CSS Fingerprinting Works
CSS fingerprinting can be performed through two methods.
JavaScript-based: Using window.matchMedia() to test media queries programmatically:
const darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const highDPI = window.matchMedia('(min-resolution: 2dppx)').matches;
const p3Gamut = window.matchMedia('(color-gamut: p3)').matches;
Pure CSS (no JavaScript): Using conditional resource loading:
@media (prefers-color-scheme: dark) {
.tracker { background-image: url('https://track.example.com/dark'); }
}
@media (prefers-color-scheme: light) {
.tracker { background-image: url('https://track.example.com/light'); }
}
The server determines the user's color scheme preference based on which URL is requested. By chaining multiple conditions, a CSS-only fingerprinting sheet can extract dozens of binary signals.
Why CSS Signals Vary
CSS media feature values depend on:
- Hardware. Monitor capabilities determine color gamut, color depth, and resolution.
- OS settings. Dark mode, high contrast, reduced motion, and accessibility settings are OS-level preferences.
- Browser configuration. Some browsers override OS preferences. Private browsing modes may modify certain values.
- Device type. Touch devices report
pointer: coarseandhover: none. Desktop devices reportpointer: fineandhover: hover.
The Consistency Problem
CSS values and JavaScript values must agree. If window.matchMedia('(prefers-color-scheme: dark)').matches returns true but screen.colorDepth suggests a light-themed configuration, or if CSS-loaded resources contradict JavaScript-reported values, the inconsistency is itself a fingerprint signal.
Common Protection Approaches and Their Limitations
Browser extensions can intercept window.matchMedia() but cannot modify CSS @media rules at the stylesheet level. A page can use pure CSS to extract values while the extension only controls the JavaScript API. This creates a detectable inconsistency.
Disabling CSS breaks the entire web. It is not a viable approach.
Standardizing preferences (always reporting light mode, no reduced motion, srgb gamut) reduces the fingerprint but creates a distinctive subset. A user who reports exactly the "default" value for every preference is uncommon and may stand out.
Using Tor Browser standardizes many CSS values across all users. But this creates a known Tor Browser fingerprint, which is identifiable.
The challenge is ensuring that CSS media features, JavaScript matchMedia() queries, and related DOM properties (like screen.colorDepth) all return consistent, controlled values from a single source.
BotBrowser's Engine-Level Approach
BotBrowser controls CSS media features at the Chromium rendering engine level, ensuring consistency across CSS and JavaScript APIs.
Unified Signal Source
When a fingerprint profile is loaded, all display-related and preference-related values come from the profile:
- Color scheme is controlled by
--bot-config-color-scheme. Both CSS@media (prefers-color-scheme: ...)rules and JavaScriptmatchMedia()queries return the same value. - Screen dimensions come from the profile's display configuration. CSS
@media (width: ...)and JavaScriptscreen.widthagree. - Device pixel ratio from the profile controls both CSS
@media (resolution: ...)and JavaScriptdevicePixelRatio. - Color depth from the profile controls CSS
@media (color: ...)and JavaScriptscreen.colorDepth. - Pointer and hover capabilities match the profiled device type. A desktop profile reports
pointer: fineandhover: hover. A mobile profile reportspointer: coarseandhover: none.
CSS-JavaScript Alignment
BotBrowser guarantees that there is no gap between what CSS @media rules see and what JavaScript APIs report. This is because both CSS media evaluation and JavaScript property access read from the same engine-level configuration, which is set by the profile.
This applies to:
@media (prefers-color-scheme: dark)βmatchMedia('(prefers-color-scheme: dark)')@media (min-resolution: 2dppx)βwindow.devicePixelRatio@media (pointer: fine)βmatchMedia('(pointer: fine)')@media (color-gamut: p3)βmatchMedia('(color-gamut: p3)')
Conditional Resource Loading
Because CSS media features are controlled at the engine level, even pure CSS fingerprinting (via conditional resource loading) returns profile-consistent values. A @media rule that loads a tracking pixel based on color scheme sees the profile's color scheme, not your system preference.
Cross-Context Consistency
CSS media features are consistent across:
- Main document stylesheets
- Shadow DOM stylesheets
matchMedia()from any JavaScript context- iframe content (same-origin and cross-origin)
- Printed media type queries
Configuration and Usage
Basic Profile Loading
chrome --bot-profile="/path/to/profile.enc" \
--user-data-dir="$(mktemp -d)"
CSS signals are configured automatically from the profile.
Color Scheme Control
# Force light mode
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-color-scheme=light
# Force dark mode
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-color-scheme=dark
Display Configuration
# Use profile's screen and window settings
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-screen=profile \
--bot-config-window=profile
# Disable device scale factor override
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-disable-device-scale-factor=true
Playwright Integration
const { chromium } = require('playwright');
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-config-color-scheme=light',
'--bot-config-screen=profile',
'--bot-config-window=profile'
]
});
const page = await browser.newPage();
await page.goto('https://example.com');
// Verify CSS signal consistency
const signals = await page.evaluate(() => ({
darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
highDPI: window.matchMedia('(min-resolution: 2dppx)').matches,
finePointer: window.matchMedia('(pointer: fine)').matches,
hoverCapable: window.matchMedia('(hover: hover)').matches,
colorDepth: screen.colorDepth,
devicePixelRatio: window.devicePixelRatio
}));
console.log(signals);
Puppeteer Integration
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
defaultViewport: null,
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-config-color-scheme=dark'
]
});
const page = await browser.newPage();
await page.goto('https://example.com');
Verification
CSS-JavaScript consistency. For each media feature, compare the CSS-evaluated value with the JavaScript-reported value. matchMedia('(prefers-color-scheme: dark)').matches should agree with the rendering behavior you observe.
Color scheme rendering. Visit a website with dark mode support. The rendering should match the --bot-config-color-scheme setting (light or dark).
DPI consistency. Compare window.devicePixelRatio with matchMedia('(resolution: 2dppx)').matches. They should agree.
Pointer and hover. Verify that matchMedia('(pointer: fine)').matches returns the expected value for your profile (desktop vs. mobile).
Cross-session stability. Run the same media query checks across multiple sessions with the same profile. All values should be identical.
Best Practices
- Set color scheme explicitly. Use
--bot-config-color-scheme=lightordarkto control the preference. The default comes from the profile, but explicit control avoids ambiguity. - Match pointer/hover to device type. Desktop profiles should report fine pointer and hover capability. Mobile profiles should report coarse pointer and no hover. BotBrowser profiles handle this automatically.
- Use profile-driven display settings.
--bot-config-screen=profileand--bot-config-window=profileensure that all dimension-related media queries match the profile. - Test with both CSS and JavaScript. Verify that media features return consistent values through both
@mediarules (by checking rendered styles) andmatchMedia()API calls. - Consider dark mode rendering impact. Websites render differently in dark mode. If your use case involves screenshots or visual comparison, ensure the color scheme is set consistently.
FAQ
Q: Can websites fingerprint me through CSS alone, without JavaScript?
A: Yes. Conditional CSS resource loading (@media rules that trigger URL requests) can extract media feature values without any JavaScript. BotBrowser protects against this because CSS media evaluation uses the same controlled values as JavaScript APIs.
Q: Does prefers-reduced-motion affect web animations?
A: Yes. When prefers-reduced-motion: reduce is active, well-designed websites disable or simplify animations. BotBrowser profiles set this preference based on the profiled device's configuration.
Q: How many bits of entropy do CSS media features provide? A: Each feature typically provides 1-3 bits (e.g., light/dark for color scheme, fine/coarse/none for pointer). Combined across 15+ features, the total can reach 10-15 bits in favorable conditions.
Q: Does BotBrowser handle the forced-colors media feature?
A: Yes. The forced-colors feature (which indicates Windows High Contrast mode) is controlled by the profile. A standard profile reports forced-colors: none.
Q: What about the dynamic-range media feature?
A: The dynamic-range feature (standard or high) indicates HDR display capability. BotBrowser profiles include this information based on the profiled device.
Q: Are CSS media features consistent in iframes? A: Yes. BotBrowser's engine-level control applies to all rendering contexts, including same-origin and cross-origin iframes. Media features are consistent across all frames.
Summary
CSS media features provide a fingerprinting surface that operates independently of JavaScript and reveals display configuration, user preferences, and interaction capabilities. BotBrowser controls all CSS media features at the Chromium engine level, ensuring that @media rules, matchMedia() queries, and related DOM properties all return consistent values derived from the loaded profile. With --bot-config-color-scheme for preference control and profile-driven display settings, BotBrowser eliminates the gap between CSS-observed and JavaScript-reported values.
For related topics, see What is Browser Fingerprinting, Screen and Window Protection, Font Fingerprint Protection, and Navigator Property Protection.