Creating a Dynamic Theme Switcher in Vue 3 with CSS Variables

Selena J
Fortitude Technologies
4 min readSep 21, 2024

As the demand for user-friendly interfaces grows, so does the expectation for applications to support multiple themes, such as dark mode and light mode. A dynamic theme switcher not only enhances user experience but also adds a level of personalization to your application. In this article, we’ll explore how to build a dynamic theme switcher in Vue 3 using CSS variables, allowing users to switch between dark mode, light mode, and custom themes.

Step 1: Defining CSS Variables for Themes

CSS variables (also known as CSS custom properties) are perfect for defining theme styles because they can be dynamically updated in real-time. Let’s define some CSS variables for light and dark themes in your src/assets/styles.css (or wherever you prefer to store your global styles).

//styles.css
:root {
--background-color: #ffffff;
--text-color: #000000;
--primary-color: #d2e8de;
}
[data-theme="dark"] {
--background-color: #090808;
--text-color: #efefef;
--primary-color: #374241;
}
[data-theme="grape"] {
--background-color: #9919c4;
--text-color: #8c64dc;
--primary-color: #490e81;
}
[data-theme="lemon"] {
--background-color: #be9523;
--text-color: #e5df6c;
--primary-color: #ea9e2c;
}

In this example, we’ve defined several themes. Each theme modifies the same set of CSS variables (--background-color, --text-color, and --primary-color).

To apply these themes globally, we’ll need to import the style sheet into main.js

//main.sj
import { createApp } from 'vue'
import App from './App.vue'
import './assets/styles.css'
createApp(App).mount('#app')

Step 2: Creating the Theme Switcher Component

Create a new component called ThemeSwitcher.vue inside your src/components directory:

<script setup>
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/stores/themeStore.js';
import { watch } from "vue";

const themeStore = useThemeStore();
const { currentTheme } = storeToRefs(themeStore);
const updateTheme = (theme) => {
themeStore.setTheme(theme);
};
watch(currentTheme, (newTheme) => {
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
</script>
<template>
<div class="theme-switcher">
<label>
<input type="radio" name="theme" value="light" @change="updateTheme('light')" :checked="currentTheme === 'light'" />
Light
</label>
<label>
<input type="radio" name="theme" value="dark" @change="updateTheme('dark')" :checked="currentTheme === 'dark'" />
Dark
</label>
<label>
<input type="radio" name="theme" value="grape" @change="updateTheme('grape')" :checked="currentTheme === 'grape'" />
Grape
</label>
<label>
<input type="radio" name="theme" value="lemon" @change="updateTheme('lemon')" :checked="currentTheme === 'lemon'" />
Lemon
</label>
<div class="card">
<h2>Card Title</h2>
<p>This is a card component. The theme switcher will change its styles.</p>
<button class="primary-button">Primary Button</button>
</div>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" placeholder="Enter your name" />
</div>
</div>
</template>
<style>
h1 {
color: var(--primary-color);
margin-bottom: 20px;
}
.card {
background-color: var(--background-color);
border: 1px solid var(--primary-color);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.primary-button {
background-color: var(--primary-color);
color: var(--text-color);
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.primary-button:hover {
background-color: darken(var(--primary-color), 10%);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: var(--text-color);
}
.form-group input {
padding: 10px;
border: 1px solid var(--primary-color);
border-radius: 4px;
width: 100%;
transition: border-color 0.3s ease, background-color 0.3s ease, color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: darken(var(--primary-color), 10%);
background-color: lighten(var(--background-color), 5%);
color: var(--text-color);
}
</style>

This component allows users to select between the custom themes. The currentTheme ref stores the currently selected theme. The updateThemefunction is called with the newly selected theme name and updates currentTheme and as well as thedata-theme attribute on the <html> element, which triggers the corresponding CSS variables. Additionally, the theme is set to be defaulted as “dark” mode. With modifications, these settings could be used with local storage or state stores to allow persistent settings.

Step 3: Using the Theme Switcher Component

To integrate the theme switcher into your application, include it in App.vueor another global component:

//App.vue
<template>
<div id="app">
<ThemeSwitcher />
</div>
</template>

<script setup>
import ThemeSwitcher from './components/ThemeSwitcher.vue';
</script>
<style>
#app {
background-color: var(--background-color);
color: var(--text-color);
padding: 20px;
transition: background-color 0.3s ease, color 0.3s ease;
}
h1 {
color: var(--primary-color);
}
</style>

The background color, text color, and primary color are now driven by the CSS variables, which change dynamically based on the active theme. The transition effect on the background and text color creates a smooth visual experience when switching themes.

Step 4: Extending and Customizing Themes

With this setup, adding new themes or customizing existing ones is straightforward. Simply define additional CSS variables for your new themes in styles.css and update the ThemeSwitcher.vue component to include the new options.

For example, to add a “blue” theme, edit styles to include:

[data-theme="blueberry"] {
--background-color: #0d4fde;
--text-color: #0ec3e3;
--primary-color: #0c408d;
}

Add the option in ThemeSwitcher.vue:

<label>
<input type="radio" name="theme" value="blueberry" @change="updateTheme('blueberry')"
:checked="currentTheme === 'blueberry'"/>
Blueberry
</label>

Conclusion

Creating a dynamic theme switcher in Vue 3 using CSS variables is a straightforward yet powerful way to enhance the user experience. By leveraging Vue 3’s reactivity and the flexibility of CSS variables, you can build a highly customizable theme management system that can easily adapt to user preferences.

This approach not only improves the visual appeal of your application but also provides users with a more personalized experience, which is becoming increasingly important in modern web development. With the ability to manage theme state globally, extend the system with additional themes, and persist user preferences across sessions, you now have the foundation for a robust theming system in your Vue 3 applications.

--

--

Selena J
Fortitude Technologies

Hey there :) I am a recent graduate from UVA and currently a Software Engineer working in full stack development. Join me as I share my tips and experiences!