DEV Community

Cover image for How to implement Theme Switcher in JavaScript
Pavel Keyzik
Pavel Keyzik

Posted on • Originally published at pavelkeyzik.com

How to implement Theme Switcher in JavaScript

In this article, you'll learn how to build a theme switcher in JavaScript. It should be a pretty easy thing to do, but you might learn something from my code as well. Let's have fun.

What cases do we need to cover?

One of the most basic scenarios we should address is changing the theme from light to dark and vice versa. The second thing we need to remember is that some people prefer to use the same settings as in the system. It's useful for those who switch between dark and light themes throughout the day. The third thing is saving user preferences; otherwise, if you refresh the page, all the settings will be set to default again.

Create a theme store

Our initial function will be createThemeStore(), which will contain nearly everything. I want to mention that this may not be the optimal approach, but hey, we're coding for fun here.

function createThemeStore(options) { // Initial mode const initialMode = options.defaultMode || 'system' // Initial state const state = { mode: initialMode, systemTheme: getSystemTheme(), theme: getThemeByMode(initialMode), } } 
Enter fullscreen mode Exit fullscreen mode

Here we create a state with only 3 variables:

  • mode: This represents the selected mode of the theme, with possible values of dark, light, or system. It allows us to determine whether to use the system's theme or not.
  • systemTheme: This holds the current value of the theme in your OS. Even if we choose a specific theme (dark or light), we still update this variable when the OS theme changes to ensure we can adjust the theme correctly when the user switches to system mode.
  • theme: This is the actual theme that the user sees, with possible values of dark or light.
  • options.defaultMode: This is used to restore correct theme preferences. For instance, you could save theme changes in localStorage and then use it as the default, ensuring that the user's preferences are retained.

Add subscriptions

When the user changes the theme or the OS theme is updated, we need a way to notify our code. This is where subscriptions come in. We need to allow subscriptions to changes in our state object. Here's the code that will help us with it. Remember, for now, we do everything inside createThemeStore().

function createThemeStore(options) { // ... // Create subscriptions object to be able notify subscribers const subscriptions = new Map() let subscriptionId = 0 // Just a unique id for every subscriber // A place where we send notification to all of our subscribers function notifyAboutThemeChange(theme) { subscriptions.forEach((notify) => { const notificationData = { mode: state.mode, theme, } notify(notificationData) // Calls subscribed function (The example how we use it will be later) }) } // A function that allows to subscribe to state changes function subscribe(callback) { subscriptionId++ subscriptions.set(subscriptionId, callback) state.systemTheme = getSystemTheme() // We'll define it later if (state.mode === 'system') { notifyAboutThemeChange(state.systemTheme) } else { notifyAboutThemeChange(state.theme) } return subscriptionId } // A function that allows to unsubscribe from changes function usubscribe(subscriptionId) { subscriptions.delete(subscriptionId) } return { subscribe, usubscribe, } } 
Enter fullscreen mode Exit fullscreen mode

Here's how it works from the consumer side.

// Create a theme store const store = createThemeStore() // Suscribe to changes const subscriptionId = store.subscribe((newTheme) => { // Here you'll be seeing theme changes console.log(newTheme) }) // When you need to unsubscribe from theme change, you just call store.usubscribe(subscriptionId) 
Enter fullscreen mode Exit fullscreen mode

Detect a system theme preferences

Now that we have our base code structure, let's add something useful. We need to define two helper functions:

  • getSystemTheme(): This should return the current OS theme dark or light
  • getThemeByMode(): This should return either dark or light based on our theme mode. For example, if the mode is set to dark, we return dark. However, when the mode is set to system, we check the system theme and respond with either dark or light, depending on the OS preferences.

It's important to note that this code won't be inside our createThemeStore() function. We're using window.matchMedia with a prefers-color-scheme media query, allowing us to determine if the OS system is set to dark or not.

const mediaQuery = '(prefers-color-scheme: dark)' // Get's current OS system function getSystemTheme() { if (window.matchMedia(mediaQuery).matches) { return 'dark' } return 'light' } // Based on user's preferences we return correct theme function getThemeByMode(mode) { if (mode === 'system') { return getSystemTheme() } return mode } function createThemeStore(options) { // ... } 
Enter fullscreen mode Exit fullscreen mode

Now, the only thing we need to do to detect our OS theme changes is to add event listener.

function createThemeStore(options) { // ... // When the OS preference has changed window.matchMedia(mediaQuery).addEventListener('change', (event) => { const prefersDarkMode = event.matches // We change system theme state.systemTheme = prefersDarkMode ? 'dark' : 'light' // And if user chose `system` mode we notify about the change // in order to be able switch theme when OS settings has changed if (state.mode === 'system') { notifyAboutThemeChange(state.systemTheme) } }) } 
Enter fullscreen mode Exit fullscreen mode

Add an ability to manually change the theme mode

We've implemented automatic theme updates whenever our OS preferences change. The only part we haven't discussed yet is manual updates of the theme mode. You'll be using this function on your dark, light, and system theme buttons.

function createThemeStore(options) { // ... function changeThemeMode(mode) { const newTheme = getThemeByMode(mode) state.mode = mode state.theme = newTheme if (state.mode === 'system') { // If the mode is system, send user a system theme notifyAboutThemeChange(state.systemTheme) } else { // Otherwise use the one that we've selected notifyAboutThemeChange(state.theme) } } return { subscribe, usubscribe, changeThemeMode, } } 
Enter fullscreen mode Exit fullscreen mode

Usage example

Our code is pure JavaScript, and you can use it anywhere. I'll demonstrate an example in React, but feel free to try it in any framework or library you enjoy.

// Create a theme store from saved theme mode // or use `system` if user hasn't changed preferences const store = createThemeStore({ defaultMode: localStorage.getItem("theme") || "system", }); function MyComponent() { // Initial active theme is `null` here, but you could use the actual value const [activeTheme, setActiveTheme] = useState(null) useEffect(() => { // Subscribe to our store changes const subscriptionId = store.subscribe((notification) => { // Update theme setActiveTheme(notification.theme) // Save selected theme mode to localStorage localStorage.setItem('theme', notification.mode) }) return () => { store.usubscribe(subscriptionId) } }, []) return ( <> <p> Active theme: <b>{activeTheme}</b>  </p>  <p>Change theme to:</p>  <button onClick={() => store.changeThemeMode("dark")}>Dark</button>  <button onClick={() => store.changeThemeMode("light")}>Light</button>  <button onClick={() => store.changeThemeMode("system")}>System</button>  <> ) } 
Enter fullscreen mode Exit fullscreen mode

Thank you!

I appreciate that you joined me on this journey, and if you were able to make it work, I'm proud of you! If something doesn't work for you or if you want to find the entire code, you can locate it here.

Top comments (0)