Detecting dark mode on every request 🌓
Tags: news, webdev, javascript, css
Published: 2022-08-08 13:05:00

I'm a huge proponent of dark color schemes! Aside from causing less eye strain due to emitting less blue light, dark pixels drain less battery life (for devices with OLED screens) and make nighttime usage much more pleasant. I immediately look for the option when opening a new app and love when it's implemented well.

Recently I came across an experimental approach to help simplify dark mode adoption on the web and wanted to share with others as it resolves a related frontend issue.

## That annoying flicker 💡

If you've ever implemented dark-mode in a frontend application, chances are you've encountered the "flickering" issue when loading the page. This is because the client doesn't know whether to render the light or dark version of the UI initially, causing it to flicker from light to dark if the user prefers that color scheme.

With SSR it's possible to get around this by storing the user's theme preference in `document.cookie`; enabling the server to check this preference on a request before responding with HTML. But even with this approach, the cookie can only be set _after_ the user first loads a page (detecting via [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) and setting this cookie), still causing the page to flicker if they prefer things to be dark.

This can also become tedious if your application spans multiple subdomains, as the cookie may need to be set for each one.

## Detecting on _every_ request 🪄

Luckily there's a solution here! (For Chrome at least, more on that later.)

As of Chromium 93, the [`Sec-CH-Prefers-Color-Scheme`](https://chromestatus.com/feature/5642300464037888) client hint header enables attaching this preference to HTTP requests. By specifying that your server allows this header:

```js
response.headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
response.headers["Vary"] = "Sec-CH-Prefers-Color-Scheme";
response.headers["Critical-CH"] = "Sec-CH-Prefers-Color-Scheme";
```

You can then get the user's preference from the request:

```js
const theme = request.headers["Sec-CH-Prefers-Color-Scheme"];
// "dark" | "light"
```

And send the appropriate markup to the client! Possibly via a class on the `body` tag:

```jsx
return (
  <body class={theme}>
    ...
  </body>
);
```

Or an inlined CSS variable:

```jsx
return (
  <body style={{ `--color-scheme: ${theme}` }}>
    ...
  </body>
);
```

Or it can integrate with the style library you're using assuming it generates styles server-side.

This is great though! No need to track color-scheme preference on the client or in your database; it can be set once on the user's device and included without any interference.

## Some limitations 💭

Unfortunately this is currently only supported on recent (93+) versions of Chromium-based browsers. As we know Chrome dominates browser share, so this translates to [roughly 70%](https://caniuse.com/?search=client%20hint) of the web.

[WebKit](https://lists.webkit.org/pipermail/webkit-dev/2021-May/031856.html) and [Mozilla](https://github.com/mozilla/standards-positions/issues/526) have requests pending to implement. Until support reaches 100% it can't (and shouldn't) be used in production, but I'm hopeful that since this delivers real value to the user experience it will get traction soon.

More information about other client hints can be found [here](https://web.dev/user-preference-media-features-headers/), as well as links to examples. If this interests you spread the word and hopefully we can see this implemented across all browsers soon!