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.
- Find the line where global css is imported. Make the following edits to that:
/* ** Global CSS */ css: ['./css/index.css'],
- Now find the head property in the same file. It should look something like this
Make the following changes to that code snippethead (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!