User-switchable custom themes with Vue.js

One very simple and working approach: Just change the css class of your body dynamically.


Today I found possibly the simplest way to solve this and it even works with SCSS (no need to have separate CSS for each theme, which is important if your themes are based on one library and you only want to define the changes), but it needs

  1. Make an .scss/.css file for each theme
  2. Make these available somewhere in the src folder, src/bootstrap-themes/dark.scss for example
  3. Import the .scss with a condition in the App.vue, in the created:, for example
if (Vue.$cookies.get('darkmode') === 'true') {
     import('../bootstrap-themes/dark.scss');
     this.nightmode = true;
} else {
     import('../bootstrap-themes/light.scss');
     this.nightmode = false;
}

When the user lands on the page, I read the cookies and see if they left nightmode enabled when they left last time and load the correct scss

When they use the switch to change the theme, this method is called, which saves the cookie and reloads the page, which will then read the cookie and load the correct scss

setTheme(nightmode) {
     this.$cookies.set("darkmode", nightmode, "7d")
     this.$router.go()
}

how about this,

https://www.mynotepaper.com/create-multiple-themes-in-vuejs

and this,

https://vuedose.tips/tips/theming-using-custom-properties-in-vuejs-components/

I think that will give you a basic idea for your project.


I will admit I had some fun with this one. This solution does not depend on Vue, but it can easily by used by Vue. Here we go!

My goal is to create a "particularly clean" dynamic insertion of <link> stylesheets which should not result in a FOUC.

I created a class (technically, it's a constructor function, but you know what I mean) called ThemeHelper, which works like this:

  • myThemeHelper.add(themeName, href) will preload a stylesheet from href (a URL) with stylesheet.disabled = true, and give it a name (just for keeping track of it). This returns a Promise that resolves to a CSSStyleSheet when the stylesheet's onload is called.
  • myThemeHelper.theme = "<theme name>"(setter) select a theme to apply. The previous theme is disabled, and the given theme is enabled. The switch happens quickly because the stylesheet has already been pre-loaded by .add.
  • myThemeHelper.theme (getter) returns the current theme name.

The class itself is 33 lines. I made a snippet that switches between some Bootswatch themes, since those CSS files are pretty large (100Kb+).

const ThemeHelper = function() {
 
  const preloadTheme = (href) => {
    let link = document.createElement('link');
    link.rel = "stylesheet";
    link.href = href;
    document.head.appendChild(link);
    
    return new Promise((resolve, reject) => {
      link.onload = e => {
        const sheet = e.target.sheet;
        sheet.disabled = true;
        resolve(sheet);
      };
      link.onerror = reject;
    });
  };
  
  const selectTheme = (themes, name) => {
    if (name && !themes[name]) {
      throw new Error(`"${name}" has not been defined as a theme.`); 
    }
    Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
  }
  
  let themes = {};

  return {
    add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
    set theme(name) { selectTheme(themes, name) },
    get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
  };
};

const themes = {
  flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
  materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
  solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
};

const themeHelper = new ThemeHelper();

let added = Object.keys(themes).map(n => themeHelper.add(n, themes[n]));

Promise.all(added).then(sheets => {
  console.log(`${sheets.length} themes loaded`);
  themeHelper.theme = "materia";
});
<h3>Click a button to select a theme</h3>

<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='materia'">Paper theme
  </button>
  
<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='flatly'">Flatly theme
</button>

<button 
  class="btn btn-primary" 
  onclick="themeHelper.theme='solar'">Solar theme
</button>

It is not hard to tell that I'm all about ES6 (and maybe I overused const just a bit :)

As far as Vue goes, you could make a component that wraps a <select>:

const ThemeHelper = function() {
 
  const preloadTheme = (href) => {
    let link = document.createElement('link');
    link.rel = "stylesheet";
    link.href = href;
    document.head.appendChild(link);
    
    return new Promise((resolve, reject) => {
      link.onload = e => {
        const sheet = e.target.sheet;
        sheet.disabled = true;
        resolve(sheet);
      };
      link.onerror = reject;
    });
  };
  
  const selectTheme = (themes, name) => {
    if (name && !themes[name]) {
      throw new Error(`"${name}" has not been defined as a theme.`); 
    }
    Object.keys(themes).forEach(n => themes[n].disabled = (n !== name));
  }
  
  let themes = {};

  return {
    add(name, href) { return preloadTheme(href).then(s => themes[name] = s) },
    set theme(name) { selectTheme(themes, name) },
    get theme() { return Object.keys(themes).find(n => !themes[n].disabled) }
  };
};

let app = new Vue({
  el: '#app',
  data() {
    return {
      themes: {
        flatly: "https://bootswatch.com/4/flatly/bootstrap.min.css",
        materia: "https://bootswatch.com/4/materia/bootstrap.min.css",
        solar: "https://bootswatch.com/4/solar/bootstrap.min.css"
      },
      themeHelper: new ThemeHelper(),
      loading: true,
    }
  },
  created() {
    // add/load themes
    let added = Object.keys(this.themes).map(name => {
      return this.themeHelper.add(name, this.themes[name]);
    });

    Promise.all(added).then(sheets => {
      console.log(`${sheets.length} themes loaded`);
      this.loading = false;
      this.themeHelper.theme = "flatly";
    });
  }
});
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <p v-if="loading">loading...</p>

  <select v-model="themeHelper.theme">
    <option v-for="(href, name) of themes" v-bind:value="name">
      {{ name }}
    </option>
  </select>
  <span>Selected: {{ themeHelper.theme }}</span>
</div>

<hr>

<h3>Select a theme above</h3>
<button class="btn btn-primary">A Button</button>

I hope this is as useful to you as it was fun for me!