Dark Mode via a Smallish Script in the Head, Avoiding FART

One of the ways you can offer Dark Mode / Light Mode on a website is to totally honor the system preference in CSS alone. So like…

@media (prefers-color-scheme: dark) {
}
@media (prefers-color-scheme: light) {
}
@media (prefers-color-scheme: no-preference) {
}Code language: CSS (css)

That’s fine, I suppose, but it’s not very fun. It’s way more fun to have a UI toggle that users can use! Plus, it’s possible that even if a user has set a preference, they might want to view your site opposite of that preference. So my thinking is that you don’t use those in CSS at all, you use a class, probably right at the document level, indicating which theme is happening:

<html lang="en" class="dark-mode">Code language: HTML, XML (xml)

So how do you get that class on there properly? I think we should:

  1. Check if there is a cookie in place (or some other storage mechanism) that has the user’s preference already in it.
  2. In lieu of that, fall back to the user’s system preference.
  3. If there is no preference, you get to pick!

The tech can get complicated! In this article, Paul Armstrong uses an Edge Function to deal with cookie management and Client Hints and such to figure out the correct class, then rewriting the HTML before it arrives at the browser with the correct class. It’s cool, but that’s a lot and I’m kinda scared of it.

I think we can get this done (and, bonus, avoid FART (Flash of inAccurate coloR Theme)) with a more basic client-side JavaScript approach. Part of the trick is running the JavaScript in the <head>, so that the class is in place before a first render. That’ll be render-blocking JavaScript, so let’s keep it as small as possible.

Let’s assume the UI toggle is:

<label for="darkMode">Dark Mode?</label>
<input id="darkMode" type="checkbox" checked>Code language: HTML, XML (xml)

Here’s my crack at the JavaScript:

<script>
  const COOKIE_NAME = 'darkmode';
  
  // Variable to prepare for unchecking the checkbox if the user prefers light mode.
  let uncheckBox = false;
  function disableDarkMode() {
    document.documentElement.classList.remove('dark-mode');
    document.documentElement.style.colorScheme = 'light';
    document.cookie = `${COOKIE_NAME}=false; expires=Fri, 31 Dec 9999 23:59:59 GMT;"`
  }
  function enableDarkMode() {
    document.documentElement.classList.add('dark-mode');
    document.documentElement.style.colorScheme = 'dark';
    document.cookie = `${COOKIE_NAME}=true; expires=Fri, 31 Dec 9999 23:59:59 GMT;"`
  }
  
  // If the user has the dark mode cookie, respect the cookie.
  if (document.cookie.split(';').some((item) => item.trim().startsWith(`${COOKIE_NAME}=true`))) {
    enableDarkMode();
    
  // If the user has the light mode cookie, respect the cookie.
  } else if (document.cookie.split(';').some((item) => item.trim().startsWith(`${COOKIE_NAME}=false`))) {
    disableDarkMode();
    uncheckBox = true;
  // If the user doesn't have the cookie, then check the system preferences. 
} else {
    // If the user prefers dark mode, or doesn't care, enable dark mode.
    if (window.matchMedia && (window.matchMedia('(prefers-color-scheme: dark)').matches || window.matchMedia('(prefers-color-scheme: no-preference)').matches)) {
      enableDarkMode();
      // User prefers light mode via system preferences.
    } else {
      disableDarkMode();
    }
  }
  // Gotta wait to do DOM stuff until DOM is ready
  addEventListener('DOMContentLoaded', (event) => {
    // window.darkMode is the <input type="checkbox" />
    window.darkMode.addEventListener("click", () => {
      if (window.darkMode.checked) {
        enableDarkMode();
      } else {
        disableDarkMode();
      }
    });
    // Make sure the UI reflects the current state.
    if (uncheckBox) {
      window.darkMode.checked = false;
    }
  });
</script>Code language: HTML, XML (xml)

With that in the <head>, the <html> element with either have a class dark-mode or it won’t. Now, in the CSS, you don’t use any @media alternations of color whatsoever, you only rely on the class name.

This relies on JavaScript being active, so that could count as a strike against it I suppose. Maybe a little improvement would be only visually revealing the UI toggle when the JavaScript runs. Improvement beyond that would need server-side code to deal with the cookies and classes.

The exact code above though is working pretty well for me as I write this:

No FART.


CodePen

I work on CodePen! I'd highly suggest you have a PRO account on CodePen, as it buys you private Pens, media uploads, realtime collaboration, and more.

Get CodePen Pro

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to Top ⬆️