DEV Community

Izzuddin Natsir
Izzuddin Natsir

Posted on

Flicker Free Dark Mode in Vite SPA

On some sites that implement automatic dark mode based on system preference, the change of colors is not immediate and result to flicker. I'll show you how to prevent that so your site visitor does not have to see the light mode if their system setting says so.

For the purpose of showcasing, I'll use React with Vite with vanilla CSS. But, you can use this technique with some styling frameworks like Tailwind CSS and other UI framework too.

First, bootstrap your project directory with Vite create-app. If you're using other package manager such as yarn or pnpm, follow the guide in Vite documentation.

npm init vite
Enter fullscreen mode Exit fullscreen mode

Then choose react as the framework.

Our focus is to implement automatic dark mode, so we will only modify index.css, App.css, and index.html.

Before that, I'll explain the cause of flicker and how the solution works. The flicker happens when color styles for dark mode is not injected early enough when the site is loaded. Or, the style changes are too much so it takes some time for it to finish.

So, the solution is to inject the theme specific styles as early as possible and make the stylesheet changes as little as possible by using CSS variables.

To inject the theme specific styles as early as possible, we're going to write an inlined IIFE script that will set theming class on html tag on page load. index.css file contains global styles and we will add CSS variables here for both .light and .dark class respectively.

The CSS variables names need to be identical for both classes. Therefore, local CSS file need not to change various color properties to adapt with <html> theme class changes.

Let's add the CSS variables:

.light {
  --page-bg: hsl(300 20% 99%);
  --lo-contrast: hsl(300 20% 99%);
  --hi-contrast: hsl(252 4% 44.8%);
}

.dark {
  --page-bg: hsl(246 6% 9%);
  --lo-contrast: hsl(253 4% 63.7%);
  --hi-contrast: hsl(256 6% 93.2%);
}

/* 
 * Duplicate body selector for the sake of clarity what style is being added.
 * Feel free to combine it with the body selector below.
 *
 * Set body background and default text color here to make sure that 
 * at least background and text color, as the major colors, are  
 * immediately set based on the theme.
 */
body {
  background: var(--page-bg);
  color: var(--hi--contrast);
}

/* CSS from Vite React template. */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}
Enter fullscreen mode Exit fullscreen mode

In case you are curious, the colors that I used above is some of Mauve palette taken from @radix-ui/colors. It's awesome by the way!

Then, let's add the script on the head tag to set the class based on your system preference or user preference saved in localStorage:

<script>
  (() => {
    const classList = document.documentElement.classList;
    const style = document.documentElement.style;
    const dark = window.matchMedia("(prefers-color-scheme: dark)");

    const update = () => {
      if (
        localStorage.theme === "dark" ||
        (!localStorage.theme && dark.matches)
      ) {

        classList.add("dark");
        style.colorScheme = "dark";
      } else {
        classList.remove("dark");
        style.colorScheme = "light";
      }
    };
    update();

    if (dark instanceof EventTarget) {
      dark.addEventListener("change", () => {
        delete localStorage.theme;
        update();
      });
    } else {
      dark.addListener(() => {
        delete localStorage.theme;
        update();
      });
    }
    window.addEventListener("storage", update);
  })();
</script>
Enter fullscreen mode Exit fullscreen mode

Now, your index.html head tag will look like this:

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script>
    (() => {
      const classList = document.documentElement.classList;
      const style = document.documentElement.style;
      // Returns `MediaQueryList` instance. It will have `matches` 
      // property if `prefers-color-scheme` is set to 'dark' 
      const dark = window.matchMedia("(prefers-color-scheme: dark)");

      const update = () => {
        if (
          localStorage.theme === "dark" ||
          (!localStorage.theme && dark.matches)
        ) {
          classList.add("dark");
          classList.remove("light");
          style.colorScheme = "dark";
        } else {
          classList.add("light");
          classList.remove("dark");
          style.colorScheme = "light";
        }
      };
      update();

      // Re-applied theme class if `prefers-color-scheme` media query is changing.
      // Modern browser `MediaQueryList` extends `EventTarget`
      if (dark instanceof EventTarget) {
        dark.addEventListener("change", () => {
          update();
        });
      } else /* Fallback for old browser */  {
        dark.addListener(() => {
          update();
        });
      }
      // Re-applied theme class if `localStorage` is being updated.
      window.addEventListener("storage", update);
    })();
  </script>
  <title>Vite App</title>
</head>
Enter fullscreen mode Exit fullscreen mode

The script being placed right after charset and viewport meta tag to make sure that the theme evaluation is being executed first before anything else.

Aaaand, congratulation! Now, your Vite site dark mode is flicker free!

Next, we will add theme switcher button on top of this. But, that's for the next article or the next iteration of this article. You can also add it yourself! You simply need to create a button that update localStorage.theme to 'dark' or 'light'.

Please let me know if you find mistakes or better solutions. Thanks for reading!

Source code: https://github.com/izznatsir/vite-auto-theme.

Top comments (0)