How to add a class to the body tag in Next.js

posted on July 31, 2021

If you are familiar with React Helmet, then you probably know that you can set attributes, such as className, on the <body> tag from any component in your code. Of course, the value of the className does not have to be static. You can use component's props to set the value dynamically:

import React from 'react';
import { Helmet } from 'react-helmet';

export default function Layout(props) {
  return (
    <div>
      <Helmet>
        <meta charSet="utf-8" />
        <title>{props.title}</title>
        <body className={props.isDark ? 'dark-mode' : 'light-mode'} />
      </Helmet>
    </div>
  );
}

Unfortunately, you can't set body class with Next's next/head, at least not officially.

If you try to do this anyway like this:

// src/pages/index.js

import Head from 'next/head';

export async function getStaticProps() {
  return { props: { isDark: true } };
}

export default function IndexPage(props) {
  return (
    <div>
      <Head>
        <title>My page title</title>
        <body className={props.isDark ? 'dark-mode' : 'light-mode'} />
      </Head>
      <p>Hello world!</p>
    </div>
  );
}

Next.js will mistakenly pull tags from <head> and put them into <body>, and it will throw the Warning: next-head-count is missing. error. Ironically, you will get the class inside the <body>:

next.js body class bug

So what should you do?

Step 1

Luckily, Next.js provides a way to customize your <html> and <body> tags. All you need to do is to use this.props.__NEXT_DATA__.props.pageProps to access the page props:

// src/pages/_document.js

import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
  render() {
    const pageProps = this.props?.__NEXT_DATA__?.props?.pageProps;
    return (
      <Html>
        <Head />
        <body className={pageProps.isDark ? 'dark-mode' : 'light-mode'}>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Now, if you remove the <body> tag from <Head>, the class will be there, and no errors will be thrown:

// src/pages/index.js

export default function IndexPage(props) {
  return (
    <div>
      <Head>
        <title>My page title</title>
      </Head>
      <p>Hello world!</p>
    </div>
  );
}
next.js body class fix

Now, you have your body class set dynamically from page props. But that's not the end of the story :)

Step 2

The problem is that the _document.js is only rendered on the server. When the browser loads the first page, it will get the correct class. But, if your need to have a different class per page, then while navigating between these pages, the class will not be updated.

Let's add another page src/pages/about.js with isDark: false and a link between About and Home pages:

// src/pages/index.js

import Link from 'next/link';
import Head from 'next/head';

export async function getStaticProps() {
  return { props: { isDark: true } };
}

export default function IndexPage(props) {
  return (
    <div>
      <Head>
        <title>Home Page</title>
      </Head>
      <h1>Home Page</h1>
      <div>
        <Link href="/about">About page</Link>
      </div>
    </div>
  );
}
// src/pages/about.js

import Link from 'next/link';
import Head from 'next/head';

export async function getStaticProps() {
  return { props: { isDark: false } };
}

export default function AboutPage() {
  return (
    <div>
      <Head>
        <title>About page</title>
      </Head>
      <h1>About page</h1>
      <div>
        <Link href="/">Home page</Link>
      </div>
    </div>
  );
}
  1. Navigate to the home page /. You will see the body has the correct dark-mode class, good.
  2. Click on the "About page" link and check the body's class. It still has the dark-mode value! This is because _document.js is only rendered on the server, and the client-side navigation via next/link happens on the client-side.
  3. Refresh the browser - you will see that now the body has the correct light-mode class. This is because when you've refreshed the page, you have requested the server to render the "about" page. And because it was generated on the server, it has the correct class.

To fix that, we can update the _app.js with a simple useEffect() hook that updates the body class:

// src/page/_app.js

import React, { useEffect } from 'react';

export default function MyApp({ Component, pageProps }) {
  useEffect(() => {
    document.body.className = pageProps.isDark ? 'dark-mode' : 'light-mode';
  });
  return <Component {...pageProps} />;
}

Now, try to go over the same steps again. You will see that the body class updates between page navigations. Yay 🎉!

You might ask yourself why wouldn't you always use the useEffect hook and throw away the changes in _document.js? The changes in _document.js ensure that when the browser loads your page, the HTML will include the body class, and your site's visitor will not need to wait for React hydration to run the useEffect hook and apply the class.