React dynamic theming without dependencies.

Fast and simple solution usnig only CSS and JS.

Working as a web developer, you will sooner or later come across a problem with theming. In my case, I had to implement a dark theme inside of an application that was already using SCSS variables. In my last project, SCSS variables theming worked well, but since those are precompiled, the theme had to be selected before the code was built. Since it was a white-label project it was not a problem, every client had a different colour scheme and styles never changed after production deployment.

This time the theme had to be changed in runtime and the requirement was for the last selected theme to be remembered between visits.

There are some external libraries to solve the theming problem but from my experience, using libraries ends up with you having the same problem only a few years later. When the library loses support and introduces some security risks that cannot be resolved by updating dependencies, you will get back to square one only now with the project much bigger and it will take much more effort to change it.

After some research, it seemed like the only viable solution without using any external libraries, was to get rid of precompiled SCSS variables and use CSS variables which could be changed by JS in runtime.

This simple example should give you an idea of how to solve a similar problem.

Solution

First, let's create react app, go to App.js and add the change theme button along with some example content.

App.js
import "./styles.css";
import { themes } from "./themes.js";

export default function App() {
  return (
    <div className="App">
      <h1>Dynamic Theme</h1>
      <button>
        Change Theme
      </button>
      <div className="example">This is an example div </div>
    </div>
  );
}

Next, add a stylesheet with CSS variables assigned to properties.

styles.css
html {
  background-color: var(--background-color);
  color: var(--text-color);
}

button {
  background-color: var(--button-color);
  color: var(--button-text-color);
}

.example {
  border: var(--example-border);
  font-style: var(--example-font-style);
  font-size: var(--example-font-size);
}

Another step is to create a js constant holding values for all variables. For better readability let's create a separate file called themes.js.

themes.js
export const themes = {
  light: {
    "--background-color": "white",
    "--text-color": "black",
    "--button-color": "gray",
    "--button-text-color": "white",
    "--example-border": "1px dotted red",
    "--example-font-size": "1rem"
  },
  dark: {
    "--background-color": "black",
    "--text-color": "white",
    "--button-color": "white",
    "--button-text-color": "black",
    "--example-border": "5px solid green",
    "--example-font-size": "2rem"
  }
};

Now the most important part. To switch variable value we need to use js function style.setProperty() on document root. Instead of applying this function for each property separately we can implement a function that iterates through keys of our specified theme object.

const setTheme = (theme) => {
  const root = document.querySelector(":root");
  Object.keys(themes[theme]).forEach((key) => {
    root.style.setProperty(key, themes[theme][key]);
  });
};

Now we need to apply the theme on the first render of out app and change it on the button click.


App.js
import { useEffect } from "react";
import "./styles.css";
import { themes } from "./themes.js";

const setTheme = (theme) => {
  const root = document.querySelector(":root");
  Object.keys(themes[theme]).forEach((key) => {
    root.style.setProperty(key, themes[theme][key]);
  });
}; 

export default function App() {

useEffect(() => {
      setTheme("light");
}, []);

  return (
    <div className="App">
      <h1>Dynamic Theme</h1>
      <button
        onClick={() => setTheme("dark")}
      >
        Change Theme
      </button>
      <div className="example">This is an example div </div>
    </div>
  );
}

This works however it would be nice if we could change the theme back and persist in the selected theme between page visits. That's where local storage comes to the rescue.

App.js
import { useEffect } from "react";
import "./styles.css";
import { themes } from "./themes.js";

const setTheme = (theme) => {
  window.localStorage.setItem("theme", theme);
  const root = document.querySelector(":root");
  Object.keys(themes[theme]).forEach((key) => {
    root.style.setProperty(key, themes[theme][key]);
  });
};

export default function App() {
  useEffect(() => {
    setTheme(window.localStorage.getItem("theme") || "light");
  }, []);

  return (
    <div className="App">
      <h1>Dynamic Theme</h1>
      <button
        onClick={() =>
          setTheme( 
                localStorage.getItem("theme") === "dark" ? 
                "light" : "dark"
          )
        }
      >
        Change Theme
      </button>
      <div className="example">This is an example div</div>
    </div>
  );
}

Now even if we close the browser selected theme will appear during our next visit.

You can change any CSS property this way and declare as many themes as you wish.

Feel free to play around with this solution on code sandbox and leave a comment.

If You plan to introduce theming in your app the sooner, you start the better. Introducing variables and themes to a big application is long painful and no fun at all ;)

I hope this solution will help with your theming problems.

Cheers!

Marek

Did you find this article valuable?

Support Marek Król by becoming a sponsor. Any amount is appreciated!