Skip to content

Instantly share code, notes, and snippets.

@jkrems
Last active May 4, 2024 23:10
Show Gist options
  • Save jkrems/e61a610e3a0f41efb2290388806f3b50 to your computer and use it in GitHub Desktop.
Save jkrems/e61a610e3a0f41efb2290388806f3b50 to your computer and use it in GitHub Desktop.
Prefers-Color-Scheme: Site Override

Motivation: What's a "User Preference"?

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:

  1. Ignore the user's real preference for their site and only honor the system-level configuration.
  2. (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.

Possible API

window.CSS.setMediaPreference(name, value)

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 the prefers- 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 to null 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.

Usage Example

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);
    });
  }
}

Persistence of the Choice

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.

Interactive Confirmation

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.

Browser UI

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.

Function Name Bikeshedding

  • window.* instead of CSS.*.
  • *.register* instead of *.set*.
  • *Prefers instead of *Preference.
  • Drop "Media" from the name, e.g. .setPrefers.

Argument Bikeshedding

  • 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 special null value. This would be especially relevant for a dictionary-style argument.

Return Value Bikeshedding

  • A promise that rejects when the value wasn't updated.
  • A promise that resolves to a boolean which signifies that the value was updated.

Alternatives

CSS.prefersMedia.colorScheme

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.

"Trust Me, This is the Real Value" HTML Attribute

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>

Other Preferences

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

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment