How to get a flickerless persistent dark mode in your Next.js app (example with MUI) (2024)

How to get a flickerless persistent dark mode in your Next.js app (example with MUI) (2)

Note that this tutorial is made with Material UI, but will work with most other UI libraries.

Here is a demo of what we are going to build:
https://buzzrank-tutorial.vercel.app/

Here is the link to the repo of the final project: https://github.com/DiMatteoL/buzzrank-tutorial

And if you are the type of person that would rather follow along a video, I’ve got just what you need:

Next.js is notorious for being just like React. “Anything that works on React will work on Next.js”, is something I often see. It’s quite true, but there is at least one thing where it is not true: Persistent Dark Modes.

Because Next.js renders React pages on the server, the standard “store the theme value in your clients local storage” approach doesn’t work properly out of the box.
And if you still do that, then you will end up facing the dreaded: Dark Mode Flicker.
Here are examples of websites that suffer form this (to test it, just switch to dark mode, and hard refresh the page, or close the page and reopen it):
MUI: https://mui.com/
Chakra UI: https://chakra-ui.com/

Ok, but how do you fix it?

TLDR: use next-themes, it will work out of the box if you don’t use a JS UI library.
But if you use a JavaScript UI library, this article might be for you. I’m using MUI for this example, but any other lib will do.

Get a runnable Next.js app (here with MUI)

I’m going to start with the starter project from the MUI GitHub repository:

Where they give us these lines to execute:

curl https://codeload.github.com/mui-org/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/nextjs-with-typescript
cd nextjs-with-typescript

Then in the project, we want to do a bit of cleaning in pages/_app.tsx for later:

Lets start by creating a PageProvider component under the src folder:

/* src/PageProvider.tsx */import { ThemeProvider } from "@mui/material";
import { ReactNode } from "react";
import theme from "./theme";
interface PageProviderProps {
children: ReactNode;
}
const PageProvider = ({ children }: PageProviderProps) => {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
};
export default PageProvider;

And then use it in our _app.tsx:

/* pages/_app.tsx */import Head from "next/head";
import { AppProps } from "next/app";
import CssBaseline from "@mui/material/CssBaseline";
import { CacheProvider, css, EmotionCache } from "@emotion/react";
import createEmotionCache from "../src/createEmotionCache";
import PageProvider from "../src/PageProvider";
import { GlobalStyles } from "@mui/material";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
}
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<PageProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...pageProps} />
</PageProvider>
</CacheProvider>
);
}

We did this because we will need to use a hook here, and we don’t want to use it in our _app.tsx file.

Use next-themes “ThemeProvider” to set your page’s “data-theme”

We can now install our theme library:

npm i next-themes

and update our _app.tsx with our css colors for our themes, and the ThemeProvider from next-themes:

/* pages/_app.tsx */import Head from "next/head";
import { AppProps } from "next/app";
import CssBaseline from "@mui/material/CssBaseline";
import { CacheProvider, css, EmotionCache } from "@emotion/react";
import createEmotionCache from "../src/createEmotionCache";
import PageProvider from "../src/PageProvider";
import { ThemeProvider } from "next-themes";
import { GlobalStyles } from "@mui/material";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
}
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
return (
<ThemeProvider>
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<PageProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<GlobalStyles
styles={css`
:root {
body {
background-color: #fff;
color: #121212;
}
}
[data-theme="dark"] {
body {
background-color: #121212;
color: #fff;
}
}
`}
/>
<Component {...pageProps} />
</PageProvider>
</CacheProvider>
</ThemeProvider>
);
}

This ThemeProvider component (from next-themes, not from MUI) prevents the rendering of the page as long as the data-theme value of our HTML page hasn’t been set. Depending on what is stored in the local storage.

And our GlobalStyles changes the page’s body colors depending on its value.

Update the theme from our app with the “useTheme” React hook

Next up, we can use the useTheme hook (from next-themes, not from MUI), and pair it with a button to properly update the theme of our page from the app.

/**/import type { NextPage } from "next";
import { css } from "@emotion/react";
import Container from "@mui/material/Container";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { useTheme } from "next-themes";
import { Button } from "@mui/material";
const Home: NextPage = () => {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<Container maxWidth="lg">
<Box
sx={{
my: 4,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Typography variant="h4" component="h1" gutterBottom>
MUI v5 + Next.js with TypeScript example
</Typography>
<Typography variant="h5" gutterBottom>
Persisted{" "}
{resolvedTheme !== theme ? `${theme} (${resolvedTheme})` : theme} mode
</Typography>
<Button
css={css`
background: linear-gradient(to top right, #2a48f3 0%, #c32cc2 100%);
`}
variant="contained"
onClick={() => setTheme(resolvedTheme === "light" ? "dark" : "light")}
>
Toggle {resolvedTheme === "light" ? "dark" : "light"} mode
</Button>
</Box>
</Container>
);
};
export default Home;

Your app is already functional and persists dark and light modes!

However, you might have noticed that most UI libs already have their own way of handling themes. And it relies on JavaScript. Sometimes even dynamically changing css classes from your components, yuck.
So, although you might think it not necessary. You probably want to follow this next step:

(Optional, but recommended) Dynamically change MUI’s theme

So this is where it gets weird.
MUI handles themes on its own. But using javascript. Which won’t do for us, because it’s way too slow, changing CSS values is way faster.
So what we did previously, is simply take the responsibility away from MUI to handle the body’s color and background-color. But that doesn’t mean that MUI shouldn’t be informed that the theme has changed as soon as possible.

To do this, we need to update the MUI ThemeProvider that we moved under our PageProvider , remember ?

/* src/PageProvider.tsx */import { ThemeProvider } from "@mui/material";
import { useTheme } from "next-themes";
import { ReactNode, useEffect, useState } from "react";
import theme, { darkTheme, lightTheme } from "./theme";
interface PageProviderProps {
children: ReactNode;
}
const PageProvider = ({ children }: PageProviderProps) => {
const { resolvedTheme } = useTheme();
const [currentTheme, setCurrentTheme] = useState(darkTheme);
useEffect(() => {
resolvedTheme === "light"
? setCurrentTheme(lightTheme)
: setCurrentTheme(darkTheme);
}, [resolvedTheme]);
return <ThemeProvider theme={currentTheme}>{children}</ThemeProvider>;
};
export default PageProvider;

And voilà ! You got yourself a Next.js app with MUI and a persisted Dark/Light Mode.

If you give a look to the finished project on the public GitHub repo.
You might notice that the file structure is different from what you ended up with after following all my instructions.
Indeed I’ve done my best to write code that is simple to understand, and that doesn’t require creating to many new files.

But I highly recommend that you do a bit of cleaning to this once your done. And if you are looking for inspiration on how to do this, you can start by looking at how I cleaned it, on the public GitHub repo.

See ya

How to get a flickerless persistent dark mode in your Next.js app (example with MUI) (2024)
Top Articles
Latest Posts
Article information

Author: Ray Christiansen

Last Updated:

Views: 5752

Rating: 4.9 / 5 (69 voted)

Reviews: 92% of readers found this page helpful

Author information

Name: Ray Christiansen

Birthday: 1998-05-04

Address: Apt. 814 34339 Sauer Islands, Hirtheville, GA 02446-8771

Phone: +337636892828

Job: Lead Hospitality Designer

Hobby: Urban exploration, Tai chi, Lockpicking, Fashion, Gunsmithing, Pottery, Geocaching

Introduction: My name is Ray Christiansen, I am a fair, good, cute, gentle, vast, glamorous, excited person who loves writing and wants to share my knowledge and understanding with you.