JS/Vue Dark Theme Switcher
Tags: javascript, css, vue, html
Published: 2020-12-30 21:42:38

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. 

```JavaScript 
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: 

```css
/* 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:

```JavaScript
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:

```JavaScript
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:

```vue
<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>
```
Original Light Theme: 
![Light](https://dev-to-uploads.s3.amazonaws.com/i/yj749yqcosrtv1dkszhf.png)

New Dark Theme:
![Dark](https://dev-to-uploads.s3.amazonaws.com/i/cv1bf2et5yfrxdm70gdq.png)