TailwindCSS Dark Mode in Next.js with Tailwind Typography Prose Classes
Tags: nextjs, react, tailwindcss
Published: 2021-01-27 02:16:37

---
title: TailwindCSS Dark Mode in Next.js with Tailwind Typography Prose Classes
published: true
description: 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.
tags: ['nextjs', 'react', 'tailwind']
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/x3a6in7kdifds26i73ia.png
canonical_url: https://egghead.io/learn/next/tailwindcss-dark-mode-nextjs-typography-prose
---

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](https://github.com/joelhooks/next-typescript-tailwind-mdx-starter/tree/1-pre-dark-mode) 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.

```javascript
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](https://www.man7.org/linux/man-pages/man7/glob.7.html) 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:

```javascript
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.

```jsx
<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](https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/). 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](https://github.com/pacocoursey/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`

```jsx
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:

```jsx
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:

```jsx
<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`:

```jsx
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:

```jsx
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`.

```jsx
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:

```jsx
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:

```jsx
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:

1. The button is completely unstyled and doesn't really look like a button
2. 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:

```jsx
<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:

1. Build times are slooooooow (on large projects they can also completely run out of memory)
2. If you visit `/hi` - an mdx file rendered and presented with TailwindCSS Typography `prose` 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.

<Callout className="p-4">
  👋 If you know how to solve this please hit me up on{' '}
  <a href="https://twitter.com/jhooks">Twitter</a>
</Callout>

For our application this is a huge hassle and requires that we run the development environment with additional memory allocated to node:

```bash
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`

```js
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:

```bash
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:

```js
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](https://github.com/tailwindlabs/tailwindcss/discussions/2793#discussioncomment-142172).

## 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`:

```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.

```jsx
<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`:

```jsx
<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']`:

```js
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](https://github.com/leerob/leerob.io/blob/main/tailwind.config.js#L33-L72).

## 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](https://github.com/eggheadio/egghead-next).

Here's the [end state of the project you've been working on in this article on Github as well](https://github.com/joelhooks/next-typescript-tailwind-mdx-starter/tree/2-hello-darkness).

If you've got any questions, please [ask them on Twitter](https://twitter.com/jhooks)!

There's also an edit link below if you'd like to send any corrections or updates directly ⭐️