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

If you are familiar with React Helmet, then you probably know that you can set attributes, such as className, to 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 so 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 in the server. This means that when browser loads the first page it will get the correct class for that page. But, if your need to have different class per page, then when navigating between these pages, the class will not be updated.

Let’s add another page src/pages/about.js with isDark: false and 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, the body’s class still has dark-mode! This is because _document.js is only rendered in the server, and the client side navigation via next/link happens on… client side.
  3. Refresh the browser - you will see that now the body has the correct light-mode class. This is because we requested the “about” page that was rendered by _document.js.

To fix that, we can update the _app.js with 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 now see that the body class updates between page navigations. Yay 🎉!

You might ask yourself why wouldn’t I always use the useEffect hook and throw away the changes in _document.js?

The changes in _document.js ensure that when 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.


comments powered by Disqus