Dark Mode with Next.js and Tailwind CSS
Jia Yi ยท 09 August 2021
5 min read
It is not uncommon to see dark mode on a web or mobile application, even Google has started integrating it into their desktop search. Some even consider both light and dark themes to be an integral part of the design.
In this article, you will learn how to add your very own dark mode using Next.js and Tailwind CSS.
Installing Next.js & Tailwind CSS
Start by creating a new base project with Next.js and Tailwind CSS. If you just want to add dark mode to your existing project, start by enabling dark mode.
npx create-next-app -e with-tailwindcss dark-mode
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
Add the following 3 lines to globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Enabling dark mode
Inside your tailwind.config.js
, change darkMode to 'class' to enable toggling of themes.
module.exports = {
purge: [],
darkMode: 'class', // change this to 'class'
theme: {
extend: {}
},
variants: {
extend: {}
},
plugins: []
};
Next-themes
We will be using a 1.6kB sized package called next-themes
to help us with the dark mode implementation.
Although it is possible to implement dark mode without the package. Here are a few reasons why next-themes
is recommended:
- Prevents flash of the wrong theme
- Sync across multiple tabs
- Stores preferred theme in localStorage
Install next-themes using npm and modify your _app.js
by wrapping using the ThemeProvider provided by next-themes.
npm i next-themes
import { ThemeProvider } from 'next-themes';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider attribute='class'>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
Integrating next-themes
Next, we create a simple navigation bar with a toggle button to switch between the themes.
const Navbar = () => {
return (
<div className='items-center'>
<div className='bg-gray-200 dark:bg-gray-600 rounded-b-md'>
<div className='flex flex-col flex-wrap p-1 mx-auto items-center md:flex-row'>
<nav className='flex flex-wrap items-center justify-start text-base '>
<ul className='items-center inline-block list-none md:inline-flex'>
<li>
<a
href='#'
className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
>
Link 1
</a>
</li>
<li>
<a
href='#'
className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
>
Link 2
</a>
</li>
</ul>
</nav>
<button className='w-auto mx-4 px-2 py-2 my-2 text-base font-medium text-white bg-gray-400 rounded-md md:ml-auto'>
Toggle
</button>
</div>
</div>
</div>
);
};
export default Navbar;
There is just one thing we need to note, right from the documentation:
...and will throw a hydration mismatch warning when rendering with SSG or SSR.
We need to ensure the component is mounted using useEffect hook before trying to render the toggle button to avoid hydration mismatch. A toggleTheme
function will be passed to the button's onClick event handler to toggle between the themes.
We will also bring in a couple of icons from Heroicons to represent our dark and light themes.
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
const Navbar = () => {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const toggleTheme = () => {
setTheme(theme === 'dark' ? 'light' : 'dark');
};
return (
<div className='items-center'>
<div className='bg-gray-200 dark:bg-gray-600 rounded-b-md'>
<div className='flex flex-col flex-wrap p-1 mx-auto items-center md:flex-row'>
<nav className='flex flex-wrap items-center justify-start text-base '>
<ul className='items-center inline-block list-none md:inline-flex'>
<li>
<a
href='#'
className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
>
Link 1
</a>
</li>
<li>
<a
href='#'
className='px-4 py-1 mr-1 text-base text-gray-800 dark:text-gray-200 rounded-md focus:shadow-outline focus:outline-none focus:ring-2 ring-offset-current ring-offset-2 hover:text-black '
>
Link 2
</a>
</li>
</ul>
</nav>
{mounted && (
<button
onClick={toggleTheme}
className='w-auto mx-4 px-2 py-2 my-2 text-base font-medium text-white bg-gray-400 rounded-md md:ml-auto'
>
{theme === 'dark' ? (
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-5 w-5'
viewBox='0 0 20 20'
fill='currentColor'
>
<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'
/>
</svg>
) : (
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-5 w-5'
viewBox='0 0 20 20'
fill='currentColor'
>
<path d='M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z' />
</svg>
)}
</button>
)}
</div>
</div>
</div>
);
};
export default Navbar;
Implemented dark mode
You'll have your very own dark mode implemented with next.js
and Tailwind CSS
.
No FOUC, no jankiness.
Perfect! ๐