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
, 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>
:
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>
);
}
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>
);
}
- Navigate to the home page
/
. You will see the body has the correctdark-mode
class, good. - 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 vianext/link
happens on the client-side. - 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.