Dark Mode with Tailwind CSS and Next.js

Posted at 24/01/2021

Last updated at 14/05/2023


This quick little piece is to help you implement, in a very streamlined fashion, a dark mode toggle in your website using Tailwind CSS and Next.js.

Before getting it down, I was battling my brain out at trying to set up this, especially given Next.js's server-side rendering stuff. Because of that, I was having trouble using the usual local.storage method for saving the preferred theme on the browser's cache. I naturally got a little overwhelmed as it seemed like a much harder task than I could've ever hoped for.

But after researching all articles around and asking for help late at night from my dev friends, I got to a very, very, simple setup using these two frameworks.

Setting up the stack

next-themes

It all goes down to this library. And it does what it says: "perfect dark mode in 2 lines of code". Easy peasy. Just install it on your project using any of these two:

$ npm install next-themes # or $ yarn add next-themes

Once it is installed, go to the _app.js file on your project, which is automatically created after installing Next, and just wrap all of the elements of the MyApp component with the ThemeProvider, just like so:

pages/app.js
import { ThemeProvider } from 'next-themes' function MyApp({ Component, pageProps }) { return ( <ThemeProvider> <Component {...pageProps} /> </ThemeProvider> ); } export default

Tailwind CSS

First, make sure to properly install Tailwind by following their guide tailored focused on a Next.js project. Once that's done, go to your tailwind.config.js file and change the dark mode property to class, just like that:

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

Now, go back to the _app.js file and just add as an attribute to the ThemeProvider a class value:

pages/_app.js
<ThemeProvider attribute="class">

Creating the theme toggle button

With everything set up, we now need to create the button that will be responsible for toggling dark mode on and off. I promise you it's not super complex! Let's start by creating a DarkModeButton.js file in your components folder (create one if you don't have one yet).

components/DarkModeButton.js
import React from 'react' export default function DarkModeButton() { return ( <button> </button> ); }

We'll use React hooks to add functionality. These will be basically responsible for toggling the dark class, mounting the theme as a whole, and also toggling the icons that we'll add within the button.

Your component should now be looking like this. Make sure you remember to import everything correctly!

components/DarkModeButton.js
import React, { useEffect, useState } from 'react'; import { useTheme } from 'next-themes'; export default function DarkModeButton() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return ( <button></button> ); }

Now comes the fun part of actually designing the button! Don't forget to add the dark: prefix to classes that are targeting the dark mode. For example, feel free to start with the styles I've added to my button.

Note that I went ahead and already added the aria-label attribute, which is important for accessibility (making the screen readers' lives easier when reading the page's elements).

components/DarkModeButton.js
import React, { useEffect, useState } from 'react'; import { useTheme } from 'next-themes'; export default function DarkModeButton() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return ( <button aria-label="Dark mode toggle button" type="button" className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7"> </button> ); }

Getting closer to the end, add the onClick event to your button element and pass the function that will objectively toggle your theme, using the setTheme hook we created.

components/DarkModeButton.js
<button aria-label="Dark mode toggle button" type="button" className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7"> onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} </button> ); }

Lastly, we'll now insert a sun and moon icon inside the button so the user knows which theme they'll get by clicking on it. To do that, we'll add an SVG element and use the mounted and theme conditionals to toggle just the path element, depending on the mode. Use whatever icons you want for this, though!

components/DarkModeButton.js
import React, { useEffect, useState } from 'react'; import { useTheme } from 'next-themes'; export default function DarkModeButton() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return ( <button aria-label="Dark mode toggle button" type="button" className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700 transition-all rounded flex items-center justify-center h-7 w-7" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} > {mounted && ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-gray-800 dark:text-gray-200" > {theme === 'dark' ? ( <path fillRule="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" clipRule="evenodd" /> ) : ( <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> )} </svg> )} </button> ); }

The mounted hook is set to false by default, meaning your website will initially render in light mode. To change that, just swap it to true instead.

components/DarkModeButton.js
const [mounted, setMounted] = useState(true);

Conclusion

Woof, that's it! ⚡️🎚🎉

You should now have a functioning dark mode toggle button on your website/app. This is a particularly cool implementation as it avoids a common pitfall with dark mode out there: the annoying flashy-ness. Tailwind also makes tailoring your design for dark mode super easy. It's tough to have neat 1:1 light and dark mode color scale mapping, and sometimes you need special treatment to preserve the interface's depth & hierarchy.

I hope this wasn't too hard to follow and that it helped you solve something that took me literally freaking weeks to figure out. Enjoy! 🤙

Be aware of hydration mismatches

If you're seeing this lil tutorial and have recently updated to the latest Next.js version (> 13.0), make sure to follow these instructions to avoid any hydration mismatches between what's on the client and on the server while using theme ternaries to toggle dark & light modes.


Thoughts about this article?

I'm all ears for feedback! Typos? Did something specific get your attention? Anything else? I'd love to hear! Drop me a note somewhere.