< Accessibility | CSS | User Experience | Browser />

User preferences: responsive that you don't see

Eroan Boyer

September 11, 2026

18 minutes

Two smartphones side by side on a desk, one displaying a blurred light interface, the other a dark interface

For fifteen years, responsive design has answered a single question: how big is the screen? Yet, every HTTP request carries a second family of signals that describe not the device, but the person: their preferred theme, their tolerance for motion, their need for contrast, their data plan. These signals are free, present in every visit, and massively ignored.

The figures from the Web Almanac 2024 highlight the gap: about 50% of mobile sites use the prefers-reduced-motion media query, 12% use prefers-color-scheme, and less than 1% use prefers-contrast. In other words, an explicit user declaration remains a dead letter on the vast majority of the web, while screen width, a simple physical constraint, is handled everywhere.

This guide covers the five preferences that the browser exposes, their modern CSS implementation, their server-side detection and its hidden cost, as well as their real impact on performance metrics. One question runs through it from beginning to end: what does your site already know about its visitors, and why isn't it doing anything with it?

Why did responsive design stop at screen width?

Responsive design was built on Level 3 media queries, standardized by the W3C in 2012, which only describe device capabilities: width, resolution, orientation. Person preferences, on the other hand, arrived with Media Queries Level 5, and their adoption remains far behind that of breakpoints.

The difference between the two families is not cosmetic. Screen width is a physical constraint that the site endures; a system preference is an explicit declaration that the user has made in their settings. Ignoring the former degrades the display, ignoring the latter is equivalent to not responding to a stated request. The fault is not of the same order.

Recent evolution confirms that the gap is slowly closing. According to the Web Almanac 2025, the use of prefers-reduced-motion has gone from 34% in 2022 to about 50%, that of prefers-color-scheme from 8% to 12%, while forced-colors is progressing by 5 points to reach 19% of sites. The most revealing detail lies elsewhere: the old -ms-high-contrast, specific to Internet Explorer and obsolete, remains present on 20% of sites. More sites declare support for a dead contrast mode than for the standard that replaced it.

This inertia is not explained by technical complexity, as these media queries are written like any other breakpoint. It is explained by a lack of knowledge of what the browser actually exposes, and it is precisely this inventory that must be drawn up before implementing anything.

What user preferences does the browser actually expose?

Five preference media features exist in Media Queries Level 5: prefers-color-scheme, prefers-reduced-motion, and prefers-contrast are Baseline and usable everywhere, prefers-reduced-transparency remains limited to Chrome and Edge since their version 119, and prefers-reduced-data is not exposed by default in any engine in 2026. The following table summarizes their respective status and role.

Media featureStatusWhat it controls
prefers-color-schemeBaseline widely availableLight or dark theme
prefers-reduced-motionBaseline widely availableAnimations, transitions, autoplay, parallax
prefers-contrastBaseline widely available (November 2024)Values more, less, custom, no-preference
prefers-reduced-transparencyChrome and Edge 119 onlybackdrop-filter, opacities, blur effects
prefers-reduced-dataSpecified, not exposed by any engineFonts, images, third-party resources

Two subtleties are worth noting. The custom value of prefers-contrast signals that a custom palette is configured, without ever saying which one: the media query does not allow reading the colors chosen by the user. As for prefers-reduced-transparency, Firefox and Safari have not implemented it, Safari citing privacy concerns, for measured use around 3.3% of page loads on Chrome.

The same interface component offered in four variants: light, dark, high contrast, and reduced motion
Four preference media queries, four renderings of the same component, without changing a line of HTML.

The case of prefers-reduced-data illustrates the gap between specification and reality. The feature is documented, testable behind a flag in Chromium, but no browser exposes it in production according to caniuse. The practical fallback has existed since 2018: the HTTP header Save-Data and the navigator.connection.saveData property, available in Chromium, are the only reliable channel for data saving, as Firefox has removed the Network Information API.

Neighboring media features to know

Beyond the five declared preferences, the browser exposes a second family of contextual signals, between capability and preference. Their list measures everything a site can know without any script.

Media featureWhat it detects
forced-colorsForced system palette, e.g., Windows high contrast
inverted-colorsOS-level color inversion
scriptingJavaScript active, restricted, or absent
updateDisplay surface refresh rate
pointer, hover, and any- variantsPointer precision and hover capability
dynamic-rangeScreen HDR capability
device-posture and display-modeFolded screen, PWA installed app

Each of these features would deserve its own development, and forced-colors will be covered later as it traps so many components. Before that, the most common task remains the light and dark theme, whose modern implementation consists of three complementary mechanisms and much less CSS than one might imagine.

How to implement a light and dark theme without duplicating all your CSS?

Three mechanisms overlap: the meta color-scheme tag in the head, the CSS color-scheme property on the root, and the light-dark() function available in all browsers since May 2024. Together, they replace the duplication of @media blocks with a single declaration per color, without a line of JavaScript.

The first reflex happens in the head, before any style information. The meta tag informs the browser of the supported schemes as early as possible during parsing, which avoids screen flashes during loading, and controls the native interface: scrollbars, form controls, default canvas background. Its CSS counterpart, the color-scheme property, has been widely available since 2022 and enables native rendering of both themes at the root level.

<meta name="color-scheme" content="light dark">

:root {
  color-scheme: light dark;
}

The light-dark() function then eliminates duplication: where each custom property had to be redeclared in an @media block, a single declaration now holds both values. It has been Baseline newly available since May 2024, with a widely available rollout expected around November 2026, which still justifies a @supports fallback for older device audiences.

/* Avant : duplication */
:root { --surface: #ffffff; }
@media (prefers-color-scheme: dark) {
  :root { --surface: #092652; }
}

/* Après : une seule déclaration */
@supports (color: light-dark(#fff, #000)) {
  :root { --surface: light-dark(#ffffff, #092652); }
}

Two pitfalls await this function. It only accepts colors, never other types of values, and above all it resolves according to the color-scheme property, not directly according to the media query: without color-scheme light dark on the root, it always returns the light branch, silently. This is the number one error found in audits, and the first thing to check when a dark theme doesn't apply.

contrast-color(), the 2026 novelty

The contrast-color() function returns black or white according to the best contrast on a given color. It is Baseline newly available since April 2026, with Chrome 147, Firefox 146, and Safari 26, and eliminates the need for a Sass function or JavaScript calculation with every brand color change.

.btn {
  background: var(--brand, #2E69E8);
  color: #ffffff; /* repli */
}
@supports (color: contrast-color(red)) {
  .btn { color: contrast-color(var(--brand, #2E69E8)); }
}

Its limits should not be overlooked: it only renders black or white, fails on gradients and images, guarantees mathematical WCAG compliance and not perceived readability, and it is ignored when forced-colors mode is active. The encrypted context invites humility: Smashing Magazine, citing the WebAIM Million, notes 83.9% of homepages reported for insufficient contrast in 2026, compared to 79.1% in 2025. The problem is worsening despite the tooling, and forced-colors mode, precisely, deserves a separate examination.

What is forced-colors mode and why does it break your components?

Forced-colors mode, detectable via the forced-colors media query, Baseline since September 2022, replaces all author colors with a limited system palette, typically that of Windows' high contrast mode. The browser substitutes colors like Canvas, CanvasText, or ButtonText, and neutralizes certain styles in the process, including drop shadows.

The mechanism, documented on MDN, goes further than a simple palette replacement: the browser also triggers the prefers-color-scheme value corresponding to the brightness of the Canvas color, so that a dark, high-contrast system theme activates both forced-colors and the site's dark theme. The most frequent pitfall boils down to one property: in forced contrast, box-shadow is forced to none. Any component whose boundary relies solely on a shadow disappears purely and simply, a ghost button first and foremost.

/* Bouton délimité par une ombre : invisible en contraste forcé */
.btn { box-shadow: 0 1px 4px rgba(9, 38, 82, .3); }

/* Correctif : bordure conditionnelle */
@media (forced-colors: active) {
  .btn { border: 1px solid ButtonText; }
}

The inverse escape hatch exists and should be used sparingly: forced-color-adjust none disables automatic adjustment for an element, at the risk of breaking the contrast the user explicitly requested. Its legitimate use is limited to rare scoped and justified cases, a logo whose colors carry the identity, or data visualization whose hues encode information.

In terms of compliance, forced contrast mode is not a named criterion of the French standard, but it acts as a revealer: the defects it exposes overlap with criteria 3.1, 10.5, and 10.9 detailed in our guide to digital accessibility obligations. Testing this mode highlights deficiencies that are already punishable elsewhere, and the next question is whether these preferences can be anticipated even before the first render.

Can user preference be known before sending the HTML?

Yes, via preference client hints, five HTTP headers supported only by Chromium: Sec-CH-Prefers-Color-Scheme, Sec-CH-Prefers-Reduced-Motion, Sec-CH-Prefers-Reduced-Transparency, Sec-CH-Prefers-Contrast, and Sec-CH-Prefers-Reduced-Data. The server can thus directly serve the correct theme, without flashes or corrective client-side scripts.

The mechanism, described by web.dev and specified by the WICG, takes place in three steps: the server announces in Accept-CH the headers it accepts, the browser sends them back on subsequent requests, and the server registers each header used in Vary so that caches store the correct variant. A third header, Critical-CH, declares a hint essential enough for the browser to re-request its initial request when it's missing, and any header listed in Critical-CH must also be included in Accept-CH and Vary.

Accept-CH: Sec-CH-Prefers-Color-Scheme
Vary: Sec-CH-Prefers-Color-Scheme
Critical-CH: Sec-CH-Prefers-Color-Scheme

On Apache hosting, these three lines are set with a few Header directives, using the same mechanism detailed in our guide to the Apache configuration file. On the WordPress side, a simple header() in PHP before sending the HTML is sufficient, and reading the received header takes one line: the value of $_SERVER['HTTP_SEC_CH_PREFERS_COLOR_SCHEME'] is dark or light, allowing the theme class to be set from server rendering.

Support remains limited to Chromium, which is not a deal-breaker since CSS still prevails elsewhere: client hints are a progressive enhancement, not a prerequisite. The real question is not browser coverage, but the cost this detection imposes on the delivery infrastructure.

Is your site as fast as your visitors expect?

Discover how we can help you

How much does server-side detection cost?

Server detection has three documented costs: an additional network round trip on the first visit via Critical-CH, a multiplication of cache variants for each header added to Vary, and an increased fingerprinting surface. The remedy for theme flashing is never free for the infrastructure, and its cost must be calculated before deployment.

The first cost hits the first visit, the most sensitive for Core Web Vitals. When Critical-CH requires a header absent from the initial request, the browser re-launches this request, a full round trip added before the first useful byte. Recent protocols erase part of it: the ACCEPT_CH frame of HTTP/2 and HTTP/3, delivered during the TLS handshake, communicates preferences at the connection level, a mechanism similar to those detailed in our article on successive HTTP protocols. Without it, the overhead is paid precisely where TTFB is decided.

The second cost affects delivery. Each header added to Vary multiplies the number of objects stored by intermediate caches: a Vary on color scheme doubles the number of variants, stacking scheme and contrast quadruples it. On a site served from a content delivery network, this fragmentation degrades the hit rate exactly like the bad settings described in our web cache guide: each additional variant dilutes shared memory among visitors.

The third cost is more discreet. The documentation for Sec-CH-Prefers-Color-Scheme classifies it among high-entropy hints, which the browser may deliberately omit: a stable preference is fingerprinting data. Firefox's Resist Fingerprinting setting causes many media queries to lie by returning default values, and these same privacy concerns explain Safari and Firefox's blocking of prefers-reduced-transparency.

The trade-off is then clearly formulated: server-side detection is only justified if theme flashing is a measured problem on the site, and on pages where cache fragmentation remains bearable. On a high-traffic editorial site cached at the edge, the remedy may cost more than the harm, and fortunately, there is still a whole area where respecting preferences brings benefits instead of costs.

How does respecting these preferences improve performance?

Respecting a preference almost always means loading less, rendering less, or calculating less: inactive theme CSS removed from the critical path, fonts not preloaded under Save-Data, backdrop-filter removed, JavaScript animations cut. Each of these levers acts on a measurable metric, from LCP to INP.

The first lever is the theme's anti-flash script. The dominant pattern is an inline blocking script in the head, which reads localStorage before the first paint to set the theme class: it blocks parsing, bypasses any cache pooling, and complicates critical CSS. The three clean outputs are the color-scheme meta tag, the light-dark() function which eliminates duplication of custom properties, and server rendering via client hints: in all three cases, the theme arrives without blocking JavaScript.

The second lever removes the inactive theme's CSS from the critical path. The recommendation from web.dev is to move the media query to the link's media attribute, so that only the active scheme's stylesheet participates in the initial render. The nuance matters: the browser still downloads the inactive stylesheet, but with low priority and without blocking rendering, according to the hierarchy detailed in our article on resource loading order. The gain is in render blocking, not in bytes.

<link rel="stylesheet" href="theme-light.css"
      media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="theme-dark.css"
      media="(prefers-color-scheme: dark)">

The same media attribute conditions font preloading. MDN documents the exact example: a preload of a woff2 file with the media prefers-reduced-data no-preference, so that the font is neither preloaded nor downloaded when the user requests less data, falling back to the system stack. Combined with the strategies from our guide to fonts, this conditioning saves tens of kilobytes on the critical path for visitors who requested it.

<link rel="preload" as="font" type="font/woff2" crossorigin
      href="/fonts/manrope.woff2"
      media="(prefers-reduced-data: no-preference)">

Two preferences directly affect the rendering pipeline. Removing backdrop-filter under prefers-reduced-transparency eliminates a costly compositing pass, and the accessibility preference here yields a measurable paint gain. As for prefers-reduced-motion, it doesn't stop at CSS: JavaScript animations and third-party libraries continue to load the main thread even when keyframes are neutralized, a mechanism at the heart of our article on the INP metric and illustrated by the carousels analyzed in our benchmark of sliders. Cutting animation at the source, via the matchMedia API, frees up main thread time, not just pixels.

Finally, there's the Save-Data signal, which can be read in two lines, on the server and client sides. The concrete adaptations it allows form a short and high-yield list:

  • do not load custom fonts;
  • defer downloading videos and iframes;
  • do not immediately open the heaviest image in a gallery;
  • reduce the number of third-party scripts;
  • disable video autoplay.
// PHP
$saveData = isset($_SERVER['HTTP_SAVE_DATA'])
  && strtolower($_SERVER['HTTP_SAVE_DATA']) === 'on';

// JavaScript
const saveData = navigator.connection?.saveData === true;

Each point in this list removes bytes or requests from the loading path, directly benefiting LCP. Yet one argument keeps coming up to justify dark mode alone, that of battery life, and it deserves a data-driven verification rather than a conviction.

Does dark mode really save battery?

Marginally in everyday use. The Purdue University study presented at MobiSys 2021 measures that at 30 to 50% brightness, the typical indoor range, switching from light to dark mode saves only 3 to 9% of the power consumed on several OLED smartphones. Substantial savings only appear at maximum brightness.

The measurements published by Dash and Hu specify the two regimes: at 100% brightness, dark mode savings increase to 39 or even 47%, a real-world scenario in direct sunlight but a minority case. The dominant factor is not the theme but the brightness itself, as reducing it from 100% to 50% divides the OLED panel's consumption by about 10, regardless of the displayed content. In other words, the brightness setting overrides the theme's effect by an order of magnitude.

The conclusion is clear: dark mode is a legitimate choice for visual comfort, not a significant eco-design lever, and presenting it as such would be greenwashing. The levers that truly impact a site's footprint are those of our responsible approach: bytes transferred, requests avoided, device lifespan. This honesty about the numbers also prevents the most common implementation errors, which are the subject of the next section.

What are the most frequent errors?

Six errors recur in almost all audits: dark mode never audited for contrast, pure black is tiring, three-state selector, movement suppressed in blocks, contrast-color() assumed active everywhere, and forced contrast mode never tested. Each has a short and verifiable fix.

The first error is believing that dark mode is an accessibility given. The WCAG minimum of 4.5:1 for standard text applies independently to each theme; ratios are not rounded up; 4.47:1 fails. Offering a toggle button does not satisfy any requirement in itself: both palettes are audited separately, using the same tools.

The second error concerns pure black. A #000000 background with white text accentuates eye strain and the halation effect, especially on OLED: the proven practice is a very dark gray, between #121212 and #1e1e1e, with off-white text.

The third error is the three-state theme selector placed in the site header. Lea Verou's argument, published on August 6, 2026, in Dark mode toggles: two states are enough, reverses the dominant logic: the underlying model must indeed have three states, but one of them is always irrelevant to the person clicking, since we only look for a toggle button when the page is bothersome.

Users do not look for solutions to problems they do not currently have.

Lea Verou, in her article Dark mode toggles: two states are enough, published on August 6, 2026

A good two-state toggle nevertheless expresses the three states of the model: it displays the resolved value when nothing is stored and its inverse when an override exists, the first click stores the override, and the click that returns to the system value removes the storage to hand control back to the OS. The tri-state selector keeps its place in a real preferences panel, where the user is in settings mode.

The fourth error neutralizes all motion with a global animation none !important. Animation is not systematically superfluous: a transitional interface, like a list that makes space for a new element, aids comprehension. The right answer is often less motion, slower, or a fade, rather than a blunt removal.

The last two errors converge on forced contrast: forgetting that contrast-color() is ignored there, and never testing the mode itself. Four points are verified in a few minutes: icons remain visible, focus remains perceptible via keyboard, states remain distinguishable without color, and no critical information lives solely in a background image. You still need to know how to trigger these modes without touching your system, which is simpler than it seems.

How to test these preferences without changing system settings?

In Chrome and Edge, the DevTools Rendering panel emulates prefers-color-scheme, prefers-reduced-motion, prefers-contrast, and forced-colors with a single click, without touching the OS. Firefox goes through about:config, with the numerical preference ui.prefersReducedMotion, 0 for full animation, 1 for reduced motion, which is taken into account immediately.

These emulations cover daily development, but they simulate the media query, not the complete environment: a real Windows machine in high contrast mode applies its palette, its user settings, and its real keyboard interactions. A final test on a real machine remains the only reliable verdict for forced-colors, at least before every major production release.

Testing tools are progressing less quickly than the platform itself, as the next chapter of these preferences is no longer in media queries but in a dedicated JavaScript API, which promises to change the very way a toggle is built.

What will change with the User Preferences API ?

The User Preferences API exposes navigator.preferences, a PreferenceManager with five objects: colorScheme, contrast, reducedMotion, reducedTransparency, and reducedData. Each offers the properties value, override, and validValues, the methods requestOverride() and clearOverride(), and a change event. It remains experimental, behind a Chrome flag in 2026.

What it solves at once is worthy of attention: no more blocking inline scripts to restore a choice, no more duplication of custom properties, and above all, an override that actually updates the CSS media query instead of bypassing it with a class on the html element. The minimal toggle is a few lines long, protected by a simple presence test.

if (navigator.preferences) {
  const cs = navigator.preferences.colorScheme;
  btn.addEventListener('click', () => {
    const target = cs.value === 'dark' ? 'light' : 'dark';
    cs.requestOverride(target).catch(() => cs.clearOverride());
  });
  cs.addEventListener('change', () => render(cs.value));
}

The status is checked before any use: the specification lives in the WICG repository maintained by Luke Warlow, attached to Media Queries Level 5, and is activated in Chrome via the Experimental Web Platform Features flag. The context argues for it: the demand for a native theme setting in the browser interface has been around for a long time, notably championed by Bramus Van Damme in Dark Mode Toggles Should be a Browser Feature, and this API is the missing technical brick.

Pure CSS or client hints: at what level to process preferences?

These signals cost nothing to read; they are already in every request, and ignoring them is like leaving unanswered a request that the user has explicitly made. The real technical trade-off is no longer whether to respect them, but at what level to process them: pure CSS when flash is tolerable, client hints when it is not and the cache supports it.

This trade-off joins that of dimensional responsiveness, whose guide to responsive design covers the complementary aspect: responsiveness answers the question of size, preferences answer that of the person. A site that masters both families of signals serves each visitor the version they have already requested, without any visible settings.

Measuring the true cost of a theme flash, a poorly calibrated Vary, or an empty animation requires instrumented diagnostics, from the field to the lab. This is precisely the scope of a web performance audit, and the deployment of these fixes is that of a performance optimization conducted rule by rule: user preferences are now a separate chapter of the diagnosis, on the same level as images or fonts.

Continue reading