DEV Community

Cover image for JS/Vue Dark Theme Switcher
Jason C
Jason C

Posted on

10

JS/Vue Dark Theme Switcher

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 ... 
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Original Light Theme:
Light

New Dark Theme:
Dark

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay