Home > Back-end >  HowTo: Toggle dark mode with TailwindCSS Vue3 Vite
HowTo: Toggle dark mode with TailwindCSS Vue3 Vite

Time:04-15

I'm a beginner regarding Vite/Vue3 and currently I am facing an issue where I need the combined knowledge of the community.

I've created a Vite/Vue3 app and installed TailwindCSS to it:

npm create vite@latest my-vite-vue-app -- --template vue
cd my-vite-vue-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Then I followed the instructions on Tailwind's homepage:

Add the paths to all of your template files in your tailwind.config.js file.
Import the newly-created ./src/index.css file in your ./src/main.js file.
Create a ./src/index.css file and add the @tailwind directives for each of Tailwind’s layers.

Now I have a working Vite/Vue3/TailwindCSS app and want to add the feature to toggle dark mode to it.

The Tailwind documentation says this can be archived by adding darkMode: 'class' to tailwind.config.js and then toggle the class dark for the <html> tag.

I made this work by using this code:

  1. Inside index.html
<html lang="en" id="html-root">
  (...)
  <body >
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
  1. Inside About.vue
<template>
  <div>
    <h1>This is an about page</h1>
    <button @click="toggleDarkMode">Toggle</botton>
  </div>
</template>

<script>
  export default {
    methods: {
      toggleDarkMode() {
        const element = document.getElementById('html-root')
        if (element.classList.contains('dark')) {
          element.classList.remove('dark')
        } else {
          element.classList.add('dark')
        }
      },
    },
  };
</script>

Yes, I know that this isn't Vue3-style code. And, yes, I know that one could do element.classList.toggle() instead of .remove() and .add(). But maybe some other beginners like me will look at this in the future and will be grateful for some low-sophisticated code to start with. So please have mercy...

Now I'll finally come to the question I want to ask the community:

I know that manipulating the DOM like this is not the Vue-way of doing things. And, of course, I want to archive my goal the correct way. But how do I do this?

Believe me I googled quite a few hours but I didn't find a solution that's working without installing this and this and this additional npm module.

But I want to have a minimalist approach. As few dependancies as possbile in order not to overwhelm me and others that want to start learning.

Having that as a background - do you guys and gals have a solution for me and other newbies? :-)

CodePudding user response:

What you're looking for is Binding Classes, but where you're getting stuck is trying to manipulate the <body> which is outside of the <div> your main Vue instance is mounted in.

Now your problem is your button is probably in a different file to your root <div id="app"> which starts in your App.vue from boilerplate code. Your two solutions are looking into state management (better for scalability), or doing some simple variable passing between parents and children. I'll show the latter:

Start with your switch component:

// DarkButton.vue
<template>
  <div>
    <h1>This is an about page</h1>
    <button @click="toggleDarkMode">Toggle</button>
  </div>
</template>

<script>
export default {
  methods: {
    toggleDarkMode() {
      this.$emit('dark-switch');
    },
  },
};
</script>

This uses component events ($emit)

Then your parent/root App.vue will listen to that toggle event and update its class in a Vue way:

<template>
  <div id="app" :>
    <p>Darkmode: {{ darkmode }}</p>
    <DarkButton @dark-switch="onDarkSwitch" />
  </div>
</template>

<script>
import DarkButton from './components/DarkButton.vue';

export default {
  name: 'App',
  components: {
    DarkButton,
  },
  data: () => ({
    darkmode: false,
  }),
  methods: {
    onDarkSwitch() {
      this.darkmode = !this.darkmode;
    },
  },
};
</script>

While tailwind say for Vanilla JS to add it into your <body>, you generally shouldn't manipulate that from that point on. Instead, don't manipulate your <body>, only go as high as your <div id="app"> with things you want to be within reach of Vue.

CodePudding user response:

The target element of your event is outside of your application. This means there is no other way to interact with it other than by querying it via the DOM available methods.

In other words, you're doing it right. If the element was within the application, than you'd simply link class to your property and let Vue handle the specifics of DOM manipulation:

 :

But it's not.


As a side note, it is really important you don't rely on whether the body element already has dark class or not to decide if you want to apply it or not. You should keep the value saved in your app's state and that should be your source of truth.
That's the Vue principle you don't want break: let data drive the DOM state, not the other way around.

In other words, it's ok to start your state (on mount) from current state of <body>, but from that point on, changes to your state will determine whether or not the class is present on the element.

vue2 example:

Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
  el: '#app',
  data: () => ({
    darkMode: document.body.classList.contains('dark')
  }),
  methods: {
    applyDarkMode() {
      document.body.classList[
        this.darkMode ? 'add' : 'remove'
      ]('dark')
    }
  },
  watch: {
    darkMode: 'applyDarkMode'
  }
})
body.dark {
  background-color: #191919;
  color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<div id="app">
  <label>
    <input type="checkbox" v-model="darkMode">
    dark mode
  </label>
</div>

vue3 example:

const {
  createApp,
  ref,
  watchEffect
} = Vue;

createApp({
  setup() {
    const darkMode = ref(document.body.classList.contains('dark'));
    const applyDarkMode = () => document.body.classList[
      darkMode.value ? 'add' : 'remove'
    ]('dark');
    watchEffect(applyDarkMode);
    return { darkMode };
  }
}).mount('#app')
body.dark {
  background-color: #191919;
  color: white;
}
<script src="https://unpkg.com/vue@next/dist/vue.global.prod.js"></script>

<div id="app">
  <label>
    <input type="checkbox" v-model="darkMode">
    dark mode
  </label>
</div>

Obviously, you might want to keep the state of darkMode in some external store, not locally, in data (and provide it in your component via computed), if you use it in more than one component.

  • Related