Supposedly, the prefers-*
media features represent the preferences of the user. And in some cases, that is true: User agents dutifully report what the user configured at the OS- or browser-level. And websites are encouraged to accommodate those wishes.
But the model breaks when the user has site-specific preferences. Suddenly the responsibility of keeping track and faithfully representing the user's preferences falls on the site and the user agent's view of the "preference" is plain wrong.
This leaves websites two options:
- Ignore the user's real preference for their site and only honor the system-level configuration.
- (Mostly) ignore the CSS media feature, outside of gathering initial default values via JS.
The result is that it's often a mistake to use the CSS media features in CSS. There's a real risk that it would be throw-away work. The moment that the site wants to provide override options to the user, it has to reimplement the theme switching using .dark
& friends.
What if prefers-*
would consistently represent what the user prefers, even if that preference is site-specific? It would be the end of .dark
workarounds, including potentially style mismatches on page load. And it would bring back the user agent as the source of truth for what the user really wants.
Overrides the given media preference for the current origin. This will update the corresponding prefers-*
media feature in CSS media queries.
- Parameters
name
: The name of the preference. It matches the CSS media feature without theprefers-
prefix. E.g. "color-scheme" to set "prefers-color-scheme".value
: Should match a possible value of the given CSS media feature. Can be set tonull
which signifies that no site-specific value is required and any override should be reset.
- Returns
undefined
. Watching a MediaQueryList is the recommended way of discovering that the new value has been applied.
Use media queries, either inside of a cross-theme CSS bundle:
@media (prefers-color-scheme: dark) {
:root {
--body-bg: darkblue;
--header-text: lightblue;
}
}
/* Hide theme selector since we don't know if it's supported. */
.theme-selector {
display: none;
}
Or to conditionally load a scheme-specific CSS bundle:
<!-- Or loading the CSS conditionally: -->
<link … media="(prefers-color-scheme: dark)" />
<link … media="not (prefers-color-scheme: dark)" />
Then, use feature detection to offer a theme override to the user:
// Enable the theme selector based on feature detection:
if (window.setMediaPreference) {
// Show theme selector UI.
document.querySelector(".theme-selector").style.display = "initial";
for (const btn of document.querySelectorAll('button[name="color-scheme"]')) {
btn.addEventListener("click", (e) => {
const colorScheme = e.currentTarget.value;
// Register the user's preference with the browser:
window.setMediaPreference("color-scheme", colorScheme);
});
}
}
The selection should be treated equivalent to a value stored in sessionStorage
or localStorage
. Which one should be at the discretion of the user / user agent. The recommended default would be to match localStorage
's lifetime.
Browsers could show an interactive confirmation prompt which could allow users to select if they want the site to control the color scheme temporarily (for the current session) or permanently. If there's no meaningful choice to be made and the site would be allowed to store data in sessionStorage
/localStorage
, a prompt wouldn't be recommended.
If a site override is active, it could be highlighted in the address bar of a desktop browser. This could allow removing the override or changing its value.
window.*
instead ofCSS.*
.*.register*
instead of*.set*
.*Prefers
instead of*Preference
.- Drop "Media" from the name, e.g.
.setPrefers
.
- Include the
prefers-*
prefix so it matches the CSS media feature 1:1. - Use
camelCase
for the name and/or value (e.g.colorScheme
). - Use a dictionary instead of separate name and value arguments, e.g.
setMediaPreference({ colorScheme: 'dark' })
. - Use an explicit
removeMediaPreference
function to reset a preference to the default instead of a specialnull
value. This would be especially relevant for a dictionary-style argument.
- A promise that rejects when the value wasn't updated.
- A promise that resolves to a boolean which signifies that the value was updated.
Writeable properties for the preferences. Looks nice but doesn't interact well with the existing media query integrations in JS.
CSS.prefersMedia.colorScheme = "dark";
Downside is that the value would either have to apply immediately or would create surprising behaviors: Assignment would succeed eventually but reads would return the old value (supposedly) until the value has actually been successfully set. This wouldn't be an issue if there's never a user interaction involved and setting the property is always allowed.
Leave the responsibility for storing the user's preference with the sites but offer a way to tell the browser about the user's real preference so that the media queries can be in sync. The attribute would only be valid on :root
.
<html preferscolorscheme="dark">
...
</html>
The above mostly talks about the color scheme. But other user preferences may also be valuable to support. E.g. a user may want to turn off excessive motion on one website without changing OS-level settings that affect other sites or apps.
prefers-contrast
prefers-reduced-motion
prefers-reduced-transparency
See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media#media_features
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Testing_media_queries
- https://developer.mozilla.org/en-US/docs/Web/API/CSS/registerProperty_static
- Rationale for a Browser-Level Color Scheme Preference - Jim Nielsen's Blog
-
A website may provide settings for controlling the color scheme instead of relying entirely on the system preference.
If only browsers had such a preference, specifically for web content. … Nah, stop kidding yourself. That will never happen. It just makes too much sense.
-
If you automatically switch your blog theme to dark colors using
@prefers-color-scheme
, please please provide a way to switch to a light theme. Reading light text on a dark background is not easier on everyone's eyes.– https://twitter.com/sarasoueidan/status/1508123765347082242
- https://github.com/pacocoursey/next-themes?tab=readme-ov-file#api & https://ui.shadcn.com/docs/dark-mode/next
- https://forums.macrumors.com/threads/apple-working-on-safari-dark-mode-toggle-for-viewing-specific-websites.2336257/