Tailwind Dark Mode Toggle with Nuxt & Vuex

Nuxt
Vuex
Tailwind CSS
Feb 26, 2022

In this post, I'll show you how to create a simple Dark Mode Toggle in Nuxt.js (also Vue.js) with Tailwind CSS in just a few steps.

Goal

The goal is to implement a simple Toggle for Light- and Dark Mode, such as in the Nuxt docs.

Dark mode toggle from Nuxt docs

How Dark Mode works in Tailwind

By default, Tailwind uses the prefers-color-scheme CSS media feature. It automatically detects if the user has requested a light or dark color theme. To learn more about this CSS media feature, you can read in the MDN documentation. You can use Tailwind's dark variant to style your site differently when dark mode is enabled.

Vue
<div class="bg-white dark:bg-slate-900" />

But since we want to switch the dark mode manually, we will follow the class-based approach.

Strategy

The class-based approach for Dark Mode works by having the dark class present in the HTML tree. To activate the class based strategy, we need to change the darkMode property in the Tailwind configuration to 'class' as following.

JavaScript
tailwind.config.js
module.exports = {
  darkMode: 'class'
};

Prepare Vuex store

To enable the toggling function, we will use the Vuex store. We will set a dark state to not only conditionally render our class later on, but also toggle it later in our actual component.

JavaScript
store/index.js
export const state = () => ({
  dark: false
});

export const getters = {
  dark: (state) => state.dark
};

export const mutations = {
  SET_DARK: (state, bool) => {
    state.dark = bool;
  }
};

Layout

Now that we have laid our foundation, we can conditionally render the dark class into any layout. For this example, it will be the Nuxt default layout.

Vue
layouts/default.vue
<template>
  <div
    id="app"
    class="bg-white dark:bg-black"
    :class="dark ? 'dark' : 'light'"
  >
    <Nuxt />
  </div>
</template>

<script>
  import { mapGetters } from 'vuex';

  export default {
    computed: {
      ...mapGetters(['dark'])
    }
  };
</script>

Dark Mode Toggle

Now we want to implement the toggle. For this, I will create a DarkModeToggle.vue component.

The SVG's I will use are from heroicons which are also by the makers of Tailwind CSS.

We will use a moon icon for Dark Mode and a sun icon for Light Mode. The icons are wrapped in a basic button element. The toggling is made possible by clicking on the button and the previously implemented SET_DARK mutation.

Vue
DarkModeToggle.vue
<template>
  <button @click="toggleDarkMode">
    <svg
      v-if="dark"
      xmlns="http://www.w3.org/2000/svg"
      class="h-6 w-6"
      viewBox="0 0 20 20"
      fill="currentColor"
    >
      <path
        d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"
      />
    </svg>
    <svg
      v-else
      xmlns="http://www.w3.org/2000/svg"
      class="h-6 w-6"
      viewBox="0 0 20 20"
      fill="currentColor"
    >
      <path
        fill-rule="evenodd"
        d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
        clip-rule="evenodd"
      />
    </svg>
  </button>
</template>

<script>
  import { mapGetters, mapMutations } from 'vuex';

  export default {
    computed: {
      ...mapGetters(['dark'])
    },

    methods: {
      ...mapMutations(['SET_DARK']),

      toggleDarkMode() {
        this.SET_DARK(!this.dark);
      }
    }
  };
</script>

Save user preference

In this example, we will use localStorage to save the user preference, as explained by Tailwind.

We will use the mounted hook to get the user preference by using the CSS media feature prefers-color-scheme and store it in localStorage. If the property doesn't exist, we will create one on mount.

Vue
DarkModeToggle.vue
<script>
  import { mapGetters, mapMutations } from 'vuex';

  export default {
    computed: {
      ...mapGetters(['dark'])
    },

    mounted() {
      if (localStorage.theme === undefined) {
        if (
          window.matchMedia &&
          window.matchMedia('(prefers-color-scheme: dark)')
            .matches
        ) {
          localStorage.theme = 'dark';
          this.SET_DARK(true);
        } else {
          localStorage.theme = 'light';
        }
      } else {
        this.SET_DARK(localStorage.theme === 'dark');
      }
    },

    methods: {
      ...mapMutations(['SET_DARK']),

      toggleDarkMode() {
        this.SET_DARK(!this.dark);
        localStorage.theme = this.dark ? 'dark' : 'light';
      }
    }
  };
</script>

Finalize

To finish the component, we add simple styling.

Vue
DarkModeToggle.vue
<template>
  <button
    class="
      p-2
      text-slate-600 hover:text-slate-800
      dark:text-slate-300 dark:hover:text-slate-100
      focus-visible:ring-2 focus-visible:ring-green-400
      rounded-lg
    "
  >...</button>
</template>