Building a dark mode theme switcher for universal Vue applications using Nuxt JS

header image

Often when making a dark mode theme toggler for our websites, we run into the issue of how to persist the theme even when the page reloads, how to prevent white flashes of the page so that the user can have a better experience. In this tutorial, we will implement dark mode in a way such that there aren’t any such problems.

For the impatient, check out my github repo for this tutorial: https://github.com/abhi12299/nuxt-dark-theme-toggle

Here’s a quick look at what we’ll be making

Now let’s get into the code. I’ll start by creating a simple Nuxt.js project using create-nuxt-app.

npx create-nuxt-app nuxt-dark-toggler

It’ll ask a series of questions that I’ve left to defaults. But make sure to add express.js as the server framework for Nuxt and also select universal mode. Rest of the options don’t matter much.

Start by installing the following dependencies

yarn add js-cookie cookie-parser

Now head to server/index.js file and make sure to use the cookie-parser middleware

// import at the top
const cookieParser = require('cookie-parser')

// this goes just before app.use(nuxt.render)
app.use(cookieParser())

With this setup, we can access client side cookies sent by the browser on our server. As you may have guessed, we’ll use cookies to persist the theme between page reloads.

Now make a file css/index.css at the root of the project. Note that you’ll have to create the css/ folder. It’ll not be created by default. Paste the following content in there.

.theme-light {
  --font-color: #000000;
  --bg-color: #ffffff;
}
.theme-dark {
  --font-color: #ffffff;
  --bg-color: #000000;
}

body {
  background-color: var(--bg-color);
  transition: background-color 0.25s ease-in;
}

As you can see, we declared 2 variables under 2 different classes, one for the font, other for the background. This is a simple demo. In a real world scenario, you’ll want more than just these variables, such as accent, foreground and shadow colors.

Now go to the nuxt.config.js file at the root of the project and make the following changes.

  1. Find the line where global css is imported. Make the following edits to that:
    /*
      ** Global CSS
      */
      css: ['./css/index.css'],
    
  2. Now find the head property in the same file. It should look something like this

    Make the following changes to that code snippet
    head (c) {
        const { req } = c.context
        let bodyClass = ''
        if (req && 'cookies' in req) {
          bodyClass = req.cookies.theme === 'dark' ? 'theme-dark' : 'theme-light'
        }
        return {
          title: 'Dark theme toggler for Nuxt.js',
          meta: [
            { charset: 'utf-8' },
            { name: 'viewport', content: 'width=device-width, initial-scale=1' },
            { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
          ],
          link: [
            { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
          ],
          bodyAttrs: {
            class: bodyClass
          }
        }
      },
    

Now this is where the magic happens. This code snippet prevents the white flash of screen that is usually found in other dark mode implementations.
Notice we made head a function. We also extracted a req object from the context property of the input passed to head. Remember, this is a universal application and this function gets run on both, the client and the server side. On the server, we have access to the incoming request from the browser. This request first goes through the cookie-parser middleware which sets a req.cookie property on the req object. With that, we can check if a cookie by the name of theme exists in the req or not. If so, we can check its value and add a class to the body tag of our server rendered HTML page. That’s exactly what the bodyAttrs property is doing at line 17.

Also notice the if condition in the code.
if (req && 'cookies' in req) { //...
We need to check if req is truthy because this function also runs on the client side. There we won’t have any req property.

From here, it’s just smooth sailing. Here’s my pages/index.vue file. It’s pretty self explanatory

<template>
  <div class="container">
    <div @click="toggleTheme">
      Toggle the theme
    </div>
  </div>
</template>

<script>
import Cookies from 'js-cookie'

export default {
  methods: {
    toggleTheme () {
      let newTheme
      if (document.body.classList.contains('theme-dark')) {
        // toggle to light theme
        document.body.classList.replace('theme-dark', 'theme-light')
        newTheme = 'light'
      } else {
        // toggle to dark theme
        document.body.classList.replace('theme-light', 'theme-dark')
        newTheme = 'dark'
      }
      // set a cookie for ssr purposes
      Cookies.set('theme', newTheme)
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  color: var(--font-color);
}
</style>

Now try running the code using yarn dev and you'll see the persistent dark mode in action You'll also notice that it stays persistent even on page reloads. Any queries/comments are welcome. Thanks!

Share :
Tags :