CSS Fingerprinting: How Stylesheets Track You
How CSS media queries such as color-gamut and prefers-color-scheme create fingerprint signals, and how to keep CSS and JavaScript values consistent.
BotBrowser Team
Prefer the maintained product doc?
This article has a matching page in the docs center. Use the docs for the canonical setup flow, current flags, and long-term reference.
CSS is usually described as a styling language, not as a tracking surface. Yet media queries let a stylesheet ask the browser about the display, the person's settings and the available input devices, and the answers differ from one group of users to another. Features such as prefers-color-scheme, prefers-reduced-motion, color-gamut, resolution, pointer and hover each reveal a small piece of device configuration. A stylesheet can read those pieces without a single line of JavaScript, which is why privacy tools that focus on scripts sometimes overlook it.
The useful question is not whether one feature identifies a person. It does not. The question is whether the values a page sees through CSS agree with the values it sees through JavaScript, and whether they stay stable for the setup you intend to present. The sections below describe the media features involved, how conditional loading exposes them without scripts, why partial protections leave gaps, how to check consistency, what site authors can do, and what BotBrowser does and does not cover.
Which media features carry signal
CSS media queries test conditions about the environment in which a page is displayed. The features that matter most for fingerprinting fall into four groups.
- Display.
resolution,color,color-index,color-gamutandmonochromedescribe pixel density, color depth and the range of colors the screen can show. - Preferences.
prefers-color-scheme,prefers-reduced-motion,prefers-contrast,prefers-reduced-transparencyandforced-colorsexpose choices made in operating system or browser settings. - Viewport and screen.
width,height,aspect-ratioandorientationdescribe the window, while the olderdevice-widthanddevice-heightdescribe the screen. - Interaction.
hover,any-hover,pointerandany-pointertell the page whether the primary input can hover and how precise it is.
Each value comes from something concrete. Monitor hardware sets the gamut, depth and resolution. Operating system settings decide dark mode, contrast and motion preferences. Browser configuration can override the system, and private browsing modes may change certain values. The device type matters as well: touch devices usually report pointer: coarse and hover: none, while desktop computers report pointer: fine and hover: hover.
How much does this reveal? The W3C Media Queries Level 5 specification acknowledges that user preference features can contribute to fingerprinting. Each feature on its own has low entropy, usually a boolean or a handful of values, often one to three bits. Combined, the picture changes. A study from TU Graz reported that CSS media features alone distinguished roughly three in ten users when more than fifteen features were queried together, and the total entropy has been estimated at 10 to 15 bits in favorable conditions. Read those numbers as reported ranges, not guarantees, because they depend on the population measured and on how many features the page asks about.
A rough calculation shows why combinations matter. Seven features with two or three values each already allow several hundred distinct combinations. Real combinations are fewer, since features correlate: a phone with a coarse pointer almost always has no hover, and a desktop computer with a fine pointer almost always has it. The effective information is therefore lower than the raw product, but it is still more than any single feature suggests.
Not every value carries the same weight. A common value, such as light mode on a standard desktop display, places a visitor in a very large group and tells a site little. A rare value, such as an active forced-colors mode, a rec2020 gamut or a request for reduced transparency, places the visitor in a much smaller group. Newer features such as dynamic-range, prefers-reduced-data and inverted-colors add further dimensions, and every feature a browser adds to the platform is one more value that a stylesheet can ask about. For that reason, the unusual combination is usually more informative than the common one, and an inconsistent combination is the most informative of all.
How a stylesheet reads values without JavaScript
A script can test a media query directly with window.matchMedia(), which reports whether a condition currently matches. This is the path most privacy tools watch.
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const dense = window.matchMedia('(min-resolution: 2dppx)').matches;
const wide = window.matchMedia('(color-gamut: p3)').matches;
The second path needs no script at all. It is conditional resource loading, where a rule inside an @media block refers to a URL.
@media (prefers-color-scheme: dark) {
.tracker {
background-image: url('https://stats.example.com/dark');
}
}
@media (prefers-color-scheme: light) {
.tracker {
background-image: url('https://stats.example.com/light');
}
}
The browser requests only the image whose condition matches, so the server learns the visitor's color scheme from which URL arrives. Repeating the pattern with more conditions lets one stylesheet collect many binary answers: dark or light, dense or standard, fine pointer or coarse pointer. Because the information travels in an ordinary request, people who block JavaScript entirely can still be partially fingerprinted this way.
Conditions can also be combined. A rule such as @media (prefers-color-scheme: dark) and (min-resolution: 2dppx) matches only when both parts are true, so a stylesheet can request one URL per combination instead of one per feature. That keeps the number of requests modest while still separating visitors by several features at once, and it means that a single mismatch between two paths can affect the result of many rules.
Two properties of this path are worth remembering. First, media queries are re-evaluated when the environment changes, so switching to dark mode in the middle of a visit triggers a new request, and the change becomes visible over time as well as at load. Second, preference features are normally shared by all frames of a page, while viewport features follow each frame's own size, so values can differ between a document and an embedded frame for legitimate reasons.
This is also why the two paths must agree. The stylesheet and the script read the same environment, and a site can compare what each one reports. If matchMedia('(prefers-color-scheme: dark)') says one thing while the resource loaded by the stylesheet says another, or if screen.colorDepth and devicePixelRatio disagree with the CSS color and resolution features, the mismatch is a signal in itself, whatever the individual values are.
Media queries are not the only part of CSS that reflects the platform. @supports rules reveal which properties and values the browser implements, system color keywords resolve to colors defined by the operating system theme, and font keywords such as system-ui resolve to a platform font. A stylesheet can use any of these to learn which operating system or browser family produced a page, and each one has to stay in line with the rest of the configuration for the same reason. A profile that describes one platform while system colors or the system font follow another produces the same kind of mismatch as a media query that disagrees with matchMedia().
Why common protections leave gaps
Several familiar approaches address part of the problem and leave the rest.
- Extensions that wrap
matchMedia()change what scripts see, but the stylesheet still evaluates@mediarules against the real display and settings. A page that uses conditional loading receives the true value from CSS and the modified value from script, so the extension itself creates the inconsistency. - Disabling CSS is not practical, because most sites depend on it to be usable at all.
- Standardized preferences, such as always reporting light mode, no reduced motion and an sRGB gamut, shrink the variation but create a distinctive group. A browser that reports the default for every preference can stand out among people whose settings differ.
- Tor Browser standardizes many values across its users, which gives a large shared group, but that group is recognizable as Tor Browser.
Browser-level resistance modes follow a similar pattern. Firefox offers a resist-fingerprinting setting that reports fixed values for several preference features, which gives the same trade-off as standardization: much less variation inside the group, but a group that is recognizable by its fixed values. None of these approaches is wrong for its intended audience. They simply answer a different question from the one this article asks, which is how to keep a chosen configuration coherent across every path a page can use.
What remains is a consistency requirement rather than a hiding requirement. CSS media features, matchMedia() and related values such as screen.colorDepth and devicePixelRatio should all derive from one source, and that source should describe a coherent device. The device pixel ratio policy explains why density is one of the values most often compared, and the guide to screen and window fingerprinting covers the dimension values that sit beside it.
Checking one profile for consistency
A useful check compares the same feature through two paths for one profile, then repeats the comparison in a later session.
- Color scheme. Open a page that has a dark theme and confirm that the rendered theme and
matchMedia('(prefers-color-scheme: dark)').matchesagree with each other and with the value you configured. - Resolution. Compare
window.devicePixelRatiowith the result ofmatchMedia('(min-resolution: 2dppx)'). Both should describe the same density. - Color depth and gamut. Compare
screen.colorDepthwith the CSScolorfeature, and checkcolor-gamutagainst the display the profile represents. - Pointer and hover. Confirm that
pointerandhoverfit the device type of the profile: fine pointer with hover for a desktop profile, coarse pointer without hover for a mobile one. - Stability. Repeat the same checks in several sessions with the same profile. The values should be identical every time.
Test the stylesheet path as well as the script path. A small local page whose @media rules change the color of a visible element shows what CSS sees, and conditional loading reads the same values. If the rendering and the script result disagree, that gap is the first thing to fix, before any other tuning.
Dark mode also changes how pages look. If you take screenshots or compare layouts between runs, set the color scheme explicitly so that results stay comparable. An unset value that follows the host machine can differ between a laptop and a server, and the difference will show up as a visual change that has nothing to do with the page.
Include embedded frames and print styles in the review. A frame evaluates its own @media rules, so a test page that embeds a second document shows whether preference features agree across frames, while viewport features follow each frame's size. Print styles use the print media type, which a page can trigger deliberately, so a stylesheet with an @media print block is another place where display-related values can be read. Treat these as the same check repeated in other contexts, not as separate problems.
When a check fails, change one thing at a time. Start from a baseline launch with only the profile loaded, record the values from both paths, then add a single option such as the color scheme or the screen source and compare again. Keep the final launch command in your notes so a later session can repeat it exactly. If the color scheme does not match what you expected, set it explicitly instead of relying on the default. If a feature query or a system color points to a different platform than you intended, check first that the profile you loaded describes the platform you meant to use, because feature availability follows the profile.
What site authors can do
Site authors have their own part in this. Media queries exist so that presentation can adapt, and that purpose is worth protecting. A few habits keep it separate from collection.
- Use media features to change styling, and avoid tying network requests to them. A different background for dark mode can usually be handled with one resource and CSS custom properties, or with the
<picture>element, without per-feature URLs. - Serve any media-conditional resource from your own origin. A request to a third-party host that differs by display or preference gives that host a signal your visitors never agreed to share.
- Do not record which conditional resource a visitor fetched unless you need it. If you do record it, say so in your privacy notice and keep the data for a short time.
- Respect the preference people have set. Reduced motion, contrast and forced colors are accessibility settings, and the article on media query preferences and user control shows how to honor them while keeping a visible way to choose an appearance.
These habits cost little, and they reduce the amount of display and preference data that leaves the page.
Testing on the author side is straightforward. Open the network panel of your browser, switch the color scheme or reduced-motion setting in the operating system, and watch which requests change. Any request that varies with a preference is worth a second look: ask whether the design needs it, whether the same result is possible with one resource, and whether the host receiving it belongs to you. Review this when a stylesheet or a third-party widget is added, since an embedded component can introduce conditional requests that the rest of the site never used.
Accessibility settings deserve special care on both sides. Reduced motion, high contrast and forced colors exist because some people cannot comfortably use a page without them. A privacy setup that overrides them to look like everyone else can make sites harder to use for the person at the keyboard, and a site that ignores them harms those people directly. When the browser you test with reports values that differ from your own settings, remember that the difference applies to that session only, and that your real preferences still belong in the browser you use every day.
What BotBrowser covers and what it leaves alone
The BotBrowser documentation on CSS Signal Consistency describes the options involved. A basic launch loads a profile, and the color scheme and screen source can be set explicitly.
chrome --bot-profile="path/to/profile.enc" \
--bot-color-scheme=dark \
--bot-screen=profile
--bot-color-scheme accepts light or dark. --bot-screen=profile uses the screen properties defined by the profile, such as color depth and dimensions, instead of the values of the host display. Both settings are optional.
Choose the profile first, because the CSS values of a session derive from it. A profile describes one device, so pick one whose platform and device type match the work you are doing, and confirm pointer and hover with the checks above. Changing the color scheme or the screen source afterwards adjusts presentation within that profile. It does not turn a desktop profile into a phone.
BotBrowser can keep CSS media and related display values aligned with the loaded profile: color scheme follows the profile and --bot-color-scheme, and display values such as color depth and screen dimensions follow the profile, so an @media rule and matchMedia() report the same profile-consistent values instead of those of the host machine. This helps you confirm that the stylesheet path and the script path agree for a given profile, and that the answer does not change between sessions. The limits are just as important. BotBrowser cannot control what a site's stylesheet requests or records, cannot make a session anonymous or unlinkable, and does not replace your own accessibility preferences such as reduced motion or contrast.
Public sources
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.