When you release a modern website one thing is clear... users expect dark mode out of the box. They aren't interested in your excuses. They don't care about the time it will take to implement, they just want dark mode. Now. 😅
What you will learn about in this article.
This article is going to explain in clear steps how to add TailwindCSS native
dark mode to a Next.js site, including the TailwindCSS Typography plugins prose
classes.
There is an assumption that you have a working knowledge of both TailwindCSS and
Next.js and a site that you'd like to implement a toggle between a dark
and a
light
theme.
To do this, you will use:
- Next.js: A React "meta-framework"
- TailwindCSS: A utility-class system for styling web applications
- TailwindCSS Typography: A plugin that provides a set of
prose
classes that provide consistently nice looking typographic defaults (useful for Markdown files, for instance) - next-themes: React Hooks based utility library for Next.js that let's you switch themes in your application.
Motivation for dark mode
With a recent relaunch of egghead.io there were daily requests for a "dark mode" for
the website. In the past our site had had a default singular dark theme, meaning a theme where the background is dark, and the text is light. The new site presented a solid
white–incredibly bright–theme that wasn't very pleasant for many users viewing experience.
Bright themes are particularly aggravating when you are working in a dark room, and
some users have vision troubles that are exacerbated by light or dark themes. This
means that the ability to choose between one or the other is often critical for some
users' ability to use the site at all.
Getting Started
If you don't have a Next.js + TailwindCSS site to work from, here's a github branch
from my Next.js Tailwind Starter that is "pre-dark mode" that you can use.
From this point we need to update some configuration files.
The Tailwind Config
tailwind.config.js
is in the root directory of the project and provides TailwindCSS
the information it needs to correctly run in your environment. The TailwindCSS team
has done a great job giving us sensible defaults, but almost every project will have
specific needs and requirements that require custom configuration.
module.exports = {
purge: ['./src/**/*.tsx'],
theme: {
typography: (theme) => ({}),
extend: {},
},
variants: {},
plugins: [require('@tailwindcss/typography')],
}
This config is almost as basic as it can be. Since you are using the TailwindCSS Typography plugin, this config let's TailwindCSS know that you want to use it. The config also has a purge
property that provides an array o globs letting TailwindCSS know which files it should analyze for purging extra classes not used in your application. If we didn't configure purging, the result would be every single class TailwindCSS has to offer being shipped with our application.
That might not be the end of the world, but it is a lot of extra bundle size that your users will never actually need.
So we purge.
After the purge configuration the see the theme
, variants
, and plugins
. Right now these sections are sparse, but that's about to change.
Enabling Dark Mode in TailwindCSS
Enabling dark mode in TailwindCSS is effectively the flip of a switch:
module.exports = {
darkMode: 'class',
purge: ['./src/**/*.tsx'],
theme: {
typography: (theme) => ({}),
extend: {},
},
variants: {},
plugins: [require('@tailwindcss/typography')],
}
By adding darkmode: 'class'
to the config, you've instructed TailwindCSS to include all of the CSS utility classes for dark mode. This enables a dark
variant that you can now add as classes to your React elements like className="bg-white dark:bg-gray-900"
and the correct class will be provided when dark
is active on your html
element.
To test out dark mode in the Next.js app, you'll need to make a couple of changes to the /src/_document.tsx
source file that is used to provide custom document structure to the Next.js application.
<Html className="dark">
<body className="dark:bg-gray-800">
<Main />
<NextScript />
</body>
</Html>
First we add the dark
class to the Html
element. This enables the dark mode for the entire application. Then we add dark:bg-gray-800
to the body
element to provide a dark background for the Next'js application when it is in dark mode.
yarn dev
will run the application, and you should see a dark background. Delete dark
from the Html
elements className
and your app should refresh with a default white background.
We've achieved dark mode! 🌑
Obviously your users aren't going to change source code to enabled toggling, so the next step is to add a button that will toggle the dark mode on and off.
Creating a theme with next-themes and React Hooks
Technically your app is going to have two themes: light
and dark
Potentially your app could have many themes up to and including hot dog stand. That's amazing if you want to provide your users with that level of flexibility! lol
There are several relatively complicated ways you might approach the problem of toggling themes. As with many things in the React.js and Next.js world, somebody else has already solved the problem very well, and for this the community favorite is next-themes which promises (and subsequently delivers) a "perfect dark mode in two lines of code".
Yes please.
yarn add next-themes
Open /src/_app.tsx
function MyApp({Component, pageProps}: AppProps) {
return (
<>
<DefaultSeo {...SEO} />
<Component {...pageProps} />
</>
)
}
Now, in /src/_app.js
import the ThemeProvider
and wrap your application Component
with it:
import {ThemeProvider} from 'next-themes'
function MyApp({Component, pageProps}: AppProps) {
return (
<>
<DefaultSeo {...SEO} />
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</>
)
}
So far, nothing has really changed in the app. Since dark
is hard-coded in your _app.tsx
and there is no mechanism to toggle the mode, your application is stuck in dark mode.
Go ahead and delete the className
from the Html
element:
<Html>
<body className="dark:bg-gray-800">
<Main />
<NextScript />
</body>
</Html>
Your application will reload, and will once again have the default white background that got us into this situation in the first place.
Toggling between light and dark modes with just a click
Open /src/pages/index.tsx
:
export default function Home() {
return (
<div>
<h1 className="text-3xl text-pink-500" css={{backgroundColor: 'teal'}}>
Welcome to Your App
</h1>
</div>
)
}
This is relatively simple React page component that is located at the root of the site. It defines a div
as a container and an h1
element with a bit of welcome text and some questionably stylish classes applied.
To make the toggle work, we need to import a hook from next-themes
, manage a little piece of state, and wire it all together in a button.
First, import the useTheme
hook:
import {useTheme} from 'next-themes'
export default function Home() {
return (
<div>
<h1 className="text-3xl text-pink-500" css={{backgroundColor: 'teal'}}>
Welcome to Your App
</h1>
</div>
)
}
Now call the useTheme
hook to gain access to theme
and setTheme
.
import {useTheme} from 'next-themes'
export default function Home() {
const {theme, setTheme} = useTheme()
return (
<div>
<h1 className="text-3xl text-pink-500" css={{backgroundColor: 'teal'}}>
Welcome to Your App
</h1>
</div>
)
}
Now, add a button
element with an onClick
handler to use as a toggle:
import {useTheme} from 'next-themes'
export default function Home() {
const {theme, setTheme} = useTheme()
return (
<div>
<h1 className="text-3xl text-pink-500" css={{backgroundColor: 'teal'}}>
Welcome to Your App
</h1>
<button onClick={}>toggle</button>
</div>
)
}
To toggle, we want to check and see what the current theme is, and set the appropriate theme based on that:
import {useTheme} from 'next-themes'
export default function Home() {
const {theme, setTheme} = useTheme()
return (
<div>
<h1 className="text-3xl text-pink-500" css={{backgroundColor: 'teal'}}>
Welcome to Your App
</h1>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
toggle
</button>
</div>
)
}
A couple of things to note are:
- The button is completely unstyled and doesn't really look like a button
- clicking it does absolutely nothing 😭
The first issue just means you need to use tailwind to make the button look awesome, but the second issue is more concerning and you need to address that to get this toggle working at all. It's a multi-faceted problem resulting from how we've configured dark mode.
In the tailwind.config.js
you set darkMode
with the class
option. This configuration property also has a media
option that uses the prefers-color-scheme
media of modern browsers and operating systems to look at how the user has configured their system. The class
option, however, means we can select and toggle the mode. In fact, you could delete the button
, set the darkMode
config to media
and call it a day.
For many use cases the class
config is the most flexible and is preferred.
In /src/_app.js
you need to tell the ThemeProvider
to use the class attribute:
<ThemeProvider attribute="class">
<Component {...pageProps} />
</ThemeProvider>
Let your app compile, refresh the page, and start toggling. Back and forth. Dazzling. A fully configured dark mode powered by Tailwind CSS in a Next.js app.
The future is now.
Solving some problems with our TailwindCSS config and dark mode
This is great. It works!
There are still a couple of problems to solve:
- Build times are slooooooow (on large projects they can also completely run out of memory)
- If you visit
/hi
- an mdx file rendered and presented with TailwindCSS Typographyprose
class, you notice that the text is black.
Slow builds with TailwindCSS Dark Mode and Next.js
This is a known problem that is at the core a webpack issue and both the Next.js team and the TailwindCSS team are aware of it. Basically, TailwindCSS + Dark Mode is a massive CSS file, and webpack hates building source maps for massive CSS files.
👋 If you know how to solve this please hit me up on{' '}
Twitter
For our application this is a huge hassle and requires that we run the development environment with additional memory allocated to node:
NODE_OPTIONS=--max-old-space-size=4048 yarn dev
Ultimately it's a small price to pay for dark mode, and will eventually be fixed upstream. It was also alleviated a bit for us by turning on purging for the dev environment in tailwind.config.css
module.exports = {
darkMode: 'class',
purge: {
enabled: true,
content: ['./src/**/*.tsx'],
},
theme: {
typography: (theme) => ({}),
extend: {},
},
variants: {},
plugins: [require('@tailwindcss/typography')],
}
These options require purge
to be an object instead of an array. We set enabled: true
and content: ['./src/**/*.tsx']
which is the same array as we had previously set purge
to.
Purging CSS means that TailwindCSS tries it best to analyze the source that you've pointed to in content
and not remove any CSS classes that you've used.
You can test it now by running the following commands:
yarn build
yarn start
Controlling the Purge
If all is well, your app should function as expected. If toggling dark mode doesn't work or appear to do anything, it could mean that the dark
CSS class variants have been stripped from your application because the dark
class isn't assigned to a className
by default.
In this example, that doesn't appear to be happened, but if you encounter this in your application where it works in development, but not in production you might need to add a safelist
property to your tailwind.config.js
purge options:
module.exports = {
darkMode: 'class',
purge: {
enabled: true,
content: ['./src/**/*.tsx'],
options: {
safelist: ['dark'], //specific classes
},
},
theme: {
typography: (theme) => ({}),
extend: {},
},
variants: {},
plugins: [require('@tailwindcss/typography')],
}
The safelist
allows you to specify classes that TailwindCSS will always keep around for you and not purge. At the time of this writing the only documentation for this is buried in some Github issue comments.
Dark Mode for TailwindCSS Typography Prose Classes
By default TailwindCSS Typography doesn't support dark mode. Prose classes are also notoriously challenging to customize. You can't just set a className
instead you need to override defaults in your tailwind.config.js
:
module.exports = {
//...
theme: {
extend: {
typography: (theme) => ({
dark: {
css: {
color: 'white',
},
},
}),
},
},
//...
}
In the theme
section of the config you a typography
property under extend
which allows us to extend the @tailwindcss/typography
plugin. The configuration property takes a function that passes in the theme
and returns an object that extends the theme for that plugin.
It makes me a little dizzy to think about, but the extension we return adds a dark
property with a css
property that sets color: 'white'
Now, in /src/layouts/index.tsx
on line 28 you'll find the prose
class being applied to a div
. This file is the default layout that mdx
files use in your application.
<div className="prose md:prose-xl max-w-screen-md">
{title && <h1 className="text-xl leading-tight">{title}</h1>}
{children}
</div>
Now add dark:prose-dark
and dark:md:prose-xl-dark
to the className
of the div
:
<div className="prose md:prose-xl dark:prose-dark dark:md:prose-xl-dark">
{title && <h1 className="text-xl leading-tight">{title}</h1>}
{children}
</div>
Refresh...
Nothing happens. No changes. There's another step in the tailwind.config.js
in the variants
config add typography: ['dark']
:
module.exports = {
darkMode: 'class',
purge: {
enabled: true,
content: ['./src/**/*.tsx'],
options: {
safelist: ['dark'], //specific classes
},
},
theme: {
typography: (theme) => ({}),
extend: {
typography: (theme) => ({
dark: {
css: {
color: 'white',
},
},
}),
},
},
variants: {
typography: ['dark'],
},
plugins: [require('@tailwindcss/typography')],
}
Voíla! You should see the body text of http://localhost:3000/hi
become white
as configured.
There are a lot of options for customizing the look and feel of your markdown. If you want some inspiration, Lee Rob has done a wonderful job for his personal site and you can check out the config here.
Summary
Users want dark mode and to set it up with TailwindCSS and Next.js it requires some configuration and basic state management. What you've done so far is just a start, and there is a lot of room to expand on the styles to make your application shine.
If you'd like to look more closely at a larger scale full-featured application (the one you are looking at right now in fact), you can checkout the repository for the egghead website on Github.
Here's the end state of the project you've been working on in this article on Github as well.
If you've got any questions, please ask them on Twitter!
There's also an edit link below if you'd like to send any corrections or updates directly ⭐️
Top comments (0)