Implementing Tailwind CSS Dark Mode Toggle with No Flicker
Cruip
Posted on September 19, 2023
Dark layouts have become increasingly popular in interface designs, and we at Cruip are proud to have embraced this trend from the beginning. We’ve developed several best-seller Tailwind CSS templates in dark skin, and in this tutorial, we will show you how to add a dark functionality to your layout bypassing one of the most common mistakes developers make: The flickering effect.
The flickering effect occurs when a user sets their preference to dark mode and navigates the site. The page initially loads in light mode but quickly switches to dark mode. This annoyance can be avoided through appropriate implementation.
Let’s see how we can add a dark layout toggle to our interfaces using HTML, React, and Vue.
Quick navigation
Dark Mode implementation with HTML and JS only
In this first part, I’ll show you how to integrate a Tailwind CSS dark mode toggle into a static website using only HTML and JavaScript.
Here are the steps we’ll follow:
- Enable Dark Mode in the Tailwind CSS configuration file
- Create an accessible toggle button using a checkbox
- Determine the default theme based on the user’s operating system preferences (
prefers-color-scheme
) - Save the user’s preference in
localStorage
to remember their choice during navigation or subsequent visits - Prevent the annoying flickering effect during page loading, also known as FOUC (flash of unstyled content)
Let’s get started!
Enabling Dark Mode in Tailwind CSS
If you’re familiar with Tailwind CSS, you might know that the framework allows enabling dark mode by simply adding darkMode: 'class'
to the Tailwind configuration file.
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
// ...
}
Once enabled, you can use the dark
class on the html
tag (or any other element) to apply the dark mode styles to all children.
Whenever you use the dark
class, Tailwind magically applies styles prefixed with dark:
to override default styles.
Creating a Dark Mode toggle switch
Now, let’s add a button that lets users switch between Light and Dark Mode. For this, we’re going with a checkbox input because it’s the most accessible and straightforward solution.
<input type="checkbox" name="light-switch" class="light-switch" />
<label for="light-switch">Switch to light / dark version</label>
By default, the checkbox is disabled (representing the light theme). To make it look like a toggle button, we’ll use some Tailwind CSS classes:
<div class="flex flex-col justify-center ml-3">
<input type="checkbox" name="light-switch" class="light-switch sr-only" />
<label class="relative cursor-pointer p-2" for="light-switch">
<svg class="dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path class="fill-slate-300" d="M7 0h2v2H7zM12.88 1.637l1.414 1.415-1.415 1.413-1.413-1.414zM14 7h2v2h-2zM12.95 14.433l-1.414-1.413 1.413-1.415 1.415 1.414zM7 14h2v2H7zM2.98 14.364l-1.413-1.415 1.414-1.414 1.414 1.415zM0 7h2v2H0zM3.05 1.706 4.463 3.12 3.05 4.535 1.636 3.12z" />
<path class="fill-slate-400" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
</svg>
<svg class="hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path class="fill-slate-400" d="M6.2 1C3.2 1.8 1 4.6 1 7.9 1 11.8 4.2 15 8.1 15c3.3 0 6-2.2 6.9-5.2C9.7 11.2 4.8 6.3 6.2 1Z" />
<path class="fill-slate-500" d="M12.5 5a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 5Z" />
</svg>
<span class="sr-only">Switch to light / dark version</span>
</label>
</div>
The toggle switch will look like this:
Next up, let’s handle the JavaScript side of things. You can either create a separate .js file or add the code directly to your HTML just before the closing body
tag.
<script>
const lightSwitches = document.querySelectorAll('.light-switch');
if (lightSwitches.length > 0) {
lightSwitches.forEach((lightSwitch, i) => {
if (localStorage.getItem('dark-mode') === 'true') {
lightSwitch.checked = true;
}
lightSwitch.addEventListener('change', () => {
const { checked } = lightSwitch;
lightSwitches.forEach((el, n) => {
if (n !== i) {
el.checked = checked;
}
});
if (lightSwitch.checked) {
document.documentElement.classList.add('dark');
localStorage.setItem('dark-mode', true);
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('dark-mode', false);
}
});
});
}
</script>
Let’s break down the above code to see how it works:
-
const lightSwitches = document.querySelectorAll('.light-switch');
gets all elements with the classlight-switch
and stores them in a variable -
if (localStorage.getItem('dark-mode') === 'true') { lightSwitch.checked = true; }
checks if the user has previously opted for dark mode. If so, we make sure the checkbox input reflects that by setting it as checked -
lightSwitch.addEventListener('change', () => { ... });
adds an event listener to the checkbox input that triggers every time the user changes its state -
const { checked } = lightSwitch;
stores the checkbox input’s value in thechecked
variable -
lightSwitches.forEach((el, n) => { if (n !== i) { el.checked = checked; } });
ensures that all checkbox inputs synchronize with the input that triggered the event, maintaining a consistent state - The final code block adds or removes the
dark
class from thehtml
element, based on the value of the checkbox input that triggered the event, and saves the user’s preference inlocalStorage
Serving the right theme on page load
Here comes the crucial part: determining the theme to display when the page first loads. We’ve got a few scenarios to account for:
- If it’s the user’s first visit, we’ll stick to the default light theme
- If it’s their first visit, but dark mode is their system preference, we’ll serve the dark theme
- For returning visitors who’ve toggled before, we’ll keep things consistent by displaying the theme they previously chose, saved in
localStorage
To make this happen, we’ll add some more JavaScript. This time, we’ll place it within the head
tag of our HTML file. By doing this, our JavaScript will run before the page loads, allowing us to avoid the unwanted flickering effect.
<script>
if (localStorage.getItem('dark-mode') === 'true' || (!('dark-mode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.querySelector('html').classList.add('dark');
} else {
document.querySelector('html').classList.remove('dark');
}
</script>
With this step, we’re all set. In the upcoming sections, I’ll show you how to integrate the Tailwind CSS dark mode toggle into dynamic sites using Next.js and Vue.
Dark Mode implementation with Next.js
JavaScript frameworks like Next.js and Vue require different strategies for the dark mode implementation. Our earlier solution won’t work.
Let’s start with Next.js. We’ll use the next-themes package, which allows us to handle dark mode very easily without worrying about blocking page rendering to check user preferences.
Of course, we must wnsure to enable dark mode in Tailwind CSS, as we saw earlier. After that, we’ll install next-themes with the terminal command npm i next-themes --save
.
Once that’s done, we need a component that lets users toggle dark mode on and off. For this, create a new file named theme-toggle.tsx
and inject the following code with the checkbox input we created earlier:
'use client'
import { useTheme } from 'next-themes'
export default function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<div className="flex flex-col justify-center ml-3">
<input
type="checkbox"
name="light-switch"
className="light-switch sr-only"
checked={theme === 'light'}
onChange={() => {
if (theme === 'dark') {
return setTheme('light')
}
return setTheme('dark')
}}
/>
<label className="relative cursor-pointer p-2" htmlFor="light-switch">
<svg className="dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path
className="fill-slate-300"
d="M7 0h2v2H7zM12.88 1.637l1.414 1.415-1.415 1.413-1.413-1.414zM14 7h2v2h-2zM12.95 14.433l-1.414-1.413 1.413-1.415 1.415 1.414zM7 14h2v2H7zM2.98 14.364l-1.413-1.415 1.414-1.414 1.414 1.415zM0 7h2v2H0zM3.05 1.706 4.463 3.12 3.05 4.535 1.636 3.12z"
/>
<path className="fill-slate-400" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
</svg>
<svg className="hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path className="fill-slate-400" d="M6.2 1C3.2 1.8 1 4.6 1 7.9 1 11.8 4.2 15 8.1 15c3.3 0 6-2.2 6.9-5.2C9.7 11.2 4.8 6.3 6.2 1Z" />
<path
className="fill-slate-500"
d="M12.5 5a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 5Z"
/>
</svg>
<span className="sr-only">Switch to light / dark version</span>
</label>
</div>
)
}
There’s not much to explain, really. We’ve simply imported useTheme
from next-themes and used the hook to get the current theme and set the theme. Easy, right?
Now, however, we need to add and remove the dark
class from the html
element based on the current theme. To do this, create a new file named theme-provider.tsx
and add this code:
'use client'
import { ThemeProvider } from 'next-themes'
export default function Theme({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
)
}
We’re not quite done yet. Since we’re in the Next.js 13 era with the app
directory, let’s get into the layout.tsx
file and include the theme provider we created earlier:
import Theme from './theme-provider'
export default function Layout({ children }) {
return (
<html suppressHydrationWarning>
<body>
<Theme>{children}</Theme>
</body>
</html>
)
}
Note that we added suppressHydrationWarning
to the html
tag to prevent Next.js from showing a warning in the console.
Dark Mode implementation with Vue
Now, let’s talk about Vue! Once again, we’ll use an external library to handle dark mode simply and quickly. In this case, the library is called VueUse, and you can install it using the command npm i @vueuse/core --save
.
After that, create a new file named ThemeToggle.vue
, and add this code:
<template>
<div>
<input type="checkbox" name="light-switch" v-model="isDark" class="light-switch sr-only" />
<label class="flex items-center justify-center cursor-pointer w-8 h-8 bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600/80 rounded-full" for="light-switch">
<svg class="w-4 h-4 dark:hidden" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path class="fill-current text-slate-400" d="M7 0h2v2H7V0Zm5.88 1.637 1.414 1.415-1.415 1.413-1.414-1.414 1.415-1.414ZM14 7h2v2h-2V7Zm-1.05 7.433-1.415-1.414 1.414-1.414 1.415 1.413-1.414 1.415ZM7 14h2v2H7v-2Zm-4.02.363L1.566 12.95l1.415-1.414 1.414 1.415-1.415 1.413ZM0 7h2v2H0V7Zm3.05-5.293L4.465 3.12 3.05 4.535 1.636 3.121 3.05 1.707Z" />
<path class="fill-current text-slate-500" d="M8 4C5.8 4 4 5.8 4 8s1.8 4 4 4 4-1.8 4-4-1.8-4-4-4Z" />
</svg>
<svg class="w-4 h-4 hidden dark:block" width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path class="fill-current text-slate-400" d="M6.2 2C3.2 2.8 1 5.6 1 8.9 1 12.8 4.2 16 8.1 16c3.3 0 6-2.2 6.9-5.2C9.7 12.2 4.8 7.3 6.2 2Z" />
<path class="fill-current text-slate-500" d="M12.5 6a.625.625 0 0 1-.625-.625 1.252 1.252 0 0 0-1.25-1.25.625.625 0 1 1 0-1.25 1.252 1.252 0 0 0 1.25-1.25.625.625 0 1 1 1.25 0c.001.69.56 1.249 1.25 1.25a.625.625 0 1 1 0 1.25c-.69.001-1.249.56-1.25 1.25A.625.625 0 0 1 12.5 6Z" />
</svg>
<span class="sr-only">Switch to light / dark version</span>
</label>
</div>
</template>
<script setup>
import { useDark } from "@vueuse/core";
const isDark = useDark({
selector: 'html',
})
</script>
Notice how we used useDark
from VueUse to get the current theme and set the theme. We also used v-model
to bind the checkbox input value to the current theme.
Unlike Next.js, in Vue, we don’t need a provider to handle dark mode! We can simply use useDark
in any component, and it will work. Simple as pie, right?
Conclusions
If you want to see how we at Cruip have previously implemented this technique, take a look at one of our templates below:
- Mosaic – Tailwind CSS admin dashboard template
- Stellar – Dark Next.js landing page template
- Quoty – Quote and invoice Tailwind CSS template
- Appy – Mobile web design template
- DevSpace – Tailwind blog template
- Docs – Documentation website template
There are a lot of them, but as we’d told you before, we’re big fans of dark layouts 🙂
Posted on September 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.