JS/Vue Dark Theme Switcher
Jason C
Posted on December 30, 2020
I recently added a dark theme to one of our internal web apps. It uses Vue 2 and Bootstrap 4. We already had a light bootstrap theme with a few customizations. So all I needed was a new dark theme and a way to switch between them.
To make life simple, I renamed the current theme to 'light.css' and added the new theme as 'dark.css'. These are both minimized bootstrap 4 themes.
Here is the code used to preload the themes into link/style sheet tags. It also handles reading the user's preferred color scheme and uses local storage in case they want to change it. When selecting a theme it sets an attribute on the body tag (data-theme
) so we can override things in CSS.
export const knownThemes = ['light', 'dark'];
const themes = {};
export class Themes {
constructor() {
const tasks = knownThemes.map(n => {
return this.preloadTheme(n).then(s => {
themes[n] = s;
})
});
Promise.all(tasks).then(t => {
this._watchSystemChanges();
const userPref = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const useTheme = localStorage.getItem("theme") || userPref || knownThemes[0];
this.select(useTheme);
});
}
preloadTheme(name) {
return new Promise((resolve, reject) => {
const id = 'theme-' + name;
let link = document.createElement('link');
link.id = id;
link.rel = "stylesheet";
link.href = '/themes/' + name + '.css';
document.head.appendChild(link);
link.onload = e => {
const sheet = document.getElementById(id);
sheet.disabled = true;
resolve(sheet);
};
link.onerror = reject;
});
}
select(name) {
if (name && !themes[name]) {
throw new Error(`"${name}" has not been defined as a theme.`);
}
let body = document.body;
Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
body.setAttribute("data-theme", name);
localStorage.setItem("theme", name);
}
_watchSystemChanges() {
if (!window.matchMedia) return;
window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (e.matches) {
this.select('dark');
} else {
this.select('light');
}
});
}
get names() {
return knownThemes;
}
}
now in our global css (outside of the themes), we can write things like this:
/* Dark theme overrides */
[data-theme="dark"] .someClass {
background-color: rgb(0,0,0);
border: 1px solid #6c757d;
color: #dee2e6;
}
[data-theme="dark"] a {
color: #96C93E;
}
[data-theme="light"] a {
color: #007bff;
}
So far, none of this is Vue specific. You could just wire this into any JS code:
import { Themes } from './themes'
let themes = new Themes();
themes.select('dark');
... etc ...
Wiring this into our Vue app was pretty straight forward. In our main.js
we just imported it, and made it a global object:
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import { Themes } from './services/themes'
// ... other imports and global config stuff
Vue.use(BootstrapVue);
Vue.prototype.$Themes = new Themes();
To give the user the ability to switch themes, I added two new options in our nav bar component:
<template>
<b-navbar toggleable="sm">
<!-- Lots of other nav stuff removed -->
<b-nav-item-dropdown >
<template v-for="theme in themes">
<b-dropdown-item class="capitalize" @click="switchTheme(theme)">{{theme}} Theme</b-dropdown-item>
</template>
</b-nav-item-dropdown>
</b-navbar>
</template>
<script>
export default {
data() {
return {
themes: []
};
},
computed: {},
mounted() {
this.themes = this.$Themes.names;
},
methods: {
switchTheme(theme) {
this.$Themes.select(theme);
}
}
};
</script>
Posted on December 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.