next-themes
next-themes copied to clipboard
Next.js 13 appDir support
Opening an issue to track support for Next.js 13's appDir
and server components.
Currently, it's possible to use next-themes
within appDir
with the following pattern:
// app/layout.tsx
import * as React from 'react'
import { Providers } from './providers'
import './globals.css'
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html>
<head>
</head>
<body>
<Providers>
{children}
</Providers>
</body>
</html>
)
}
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
{children}
</ThemeProvider>
)
}
This works for the most part, including accessing and changing the theme via useTheme
.
However, during next dev
, we get the following console error hinting at hydration problems:
Warning: Extra attributes from the server: data-theme,style
at html
at ReactDevOverlay (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:53:9)
at HotReload (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:19:11)
at Router (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/app-router.js:74:11)
at ErrorBoundaryHandler (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:28:9)
at ErrorBoundary (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/components/error-boundary.js:40:11)
at AppRouter
at ServerRoot (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:113:11)
at RSCComponent
at Root (webpack-internal:///./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/client/app-index.js:130:11)
And during production (hosted on Vercel), the following hydration errors seem to randomly appear regardless of how much I pair down the example so long as I'm using next-themes
in this way:
Error: Minified React error #418; visit https://reactjs.org/docs/error-decoder.html?invariant=418 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at zh (796-3c8db907cc96f9a4.js:9:55756)
at ...
#145 looks to be a cookies-based approach to solving this issue, but the PR looks stalled and I'm not sure if it's a working solution or even the best approach. So I wanted to open up an issue here to track suggestions & progress.
Thanks!
Can't speak to all the issues brought up, but the hydration errors occur because of the way that next/react streams the html. If you replace the <script>
tag in the ThemeScript
component with the <NextScript />
component, it should hydrate correctly.
#145 was working with an earlier build of Next.js, but it definitely looks like using <NextScript />
can clean that pr up quite a bit now. I'm gonna try to update it this week
Thanks @WITS 🙏
@leerob would you be able to provide any guidance on the best solution here (or forward to someone who might)?
That pr should be updated to work with Next.js 13.0.3
now. I'm still looking into making further improvements, but you can test the changes through the npm package @wits/next-themes prior to the pr being merged. (Or try running npm i "next-themes@npm:@wits/next-themes"
if you want to keep the same imports in your code.)
I'd like to provide some more context on why the current approach uses cookies. Unfortunately, trying to get this working in the app directory without ensuring that the server is rendering the same <html>
attributes results in errors. So I reached out to @pacocoursey as well as some members of the Next.js team before starting this work, and they agreed that a cookie-based approach would be the right move.
next/script
is a great call! It wasn't available for the app directory when I first started writing these changes, but it looks like using it with stategy="beforeInteractive"
works here. However, doing so inside of a client component (like the existing ThemeProvider
still causes it to run too late and the page will flicker un-themed content. Also, by default, that script relies on window.matchMedia
to determine the system theme of the user. That isn't available on the server, so without cookies we still get errors due to a mismatch between client and server rendering during fast refreshes.
Update: Please make sure you use the
<ServerThemeProvider>
(as outlined here) if you're trying this out. If you want to have theme switcher UI, you'll need both the<ServerThemeProvider>
in the layout and a<ThemeProvider>
in an interactive ("use client"
) component.
Update 2: The fork above is no longer being maintained! Please see this comment for instructions on how to use the stable version of
next-themes
withappDir
.
Appreciate the in-depth breakdown @WITS.
I just tried @wits/next-themes
on my project, and while I can verify it's taking effect, I'm not seeing any changes with regards to the error messages. On next dev
, the same hydration error occurs w.r.t. extra html attributes, and on production, the same hydration errors appear sporadically (same as previously).
This could be a problem with my app, of course, but the hydration errors during next dev
give me pause.
Would you be able to share your updated app/layout.tsx
(or a simplified version of it) so I can reproduce the issue locally?
Definitely not a minimal repro, but my source is here: https://github.com/transitive-bullshit/next-movie/blob/main/app/layout.tsx. See RootLayoutProviders.
I currently have my usage of next-themes
commented out in the providers and in a few places in the code because no matter what I tried, I would get sporadic hydration errors on prod that would cause the entire client-side JS to stop working — resulting in a dead app for users. It should be easy to re-enable once next-themes
is patched.
Ah, I should have clarified when I explained the approach above that it requires a new component. Can you try wrapping the <html>
tag in your layout.tsx
file with the <ServerThemeProvider>
from @wits/next-themes
and see if that works?
(There's a section in the updated README with an example.)
Ahhh good catch.
Okay, so after doing that and re-enabling my usage of next-themes@npm:@wits/themes
, I'm seeing the following.
Dev
During next dev
the first few times it loads is fine, but as soon as I toggle dark mode client-side and hard refresh, I start seeing mismatched hydration warnings in the dev console:


Prod
It works well hydration-wise 💯 I haven't been able to reproduce any hydration errors when deployed as a preview build to Vercel: https://next-movie-hrwhglaz6-saasify.vercel.app/
However, I'm now seeing different, consistent 500 errors from Vercel using this branch with next-themes
enabled that doesn't repro on main
:
Error: invariant: invalid Cache-Control duration provided: 0 < 1

Example: https://next-movie-hrwhglaz6-saasify.vercel.app/titles/424
@WITS I'm going to hold off on re-enabling dark mode toggle in prod for now. I'm not too worried about the next dev
errors, so long as it works on prod, though we should probably get them fixed before merging #145. If you want to test locally, check out https://github.com/transitive-bullshit/next-movie/tree/feature/next-themes-beta and set DATABASE_URL
to a Postgres instance. You may also need to do npx prisma db push
or npx prisma generate
.
I think this cache-control issue invariant issue is likely a bug with next@canary
, though it's strange that it only repros on this branch where the only changes involve enabling next-themes
.
I created a few Next.js issues to track the errors I'm running into. Not sure what else I can do to help.

[0] event - compiled client and server successfully in 837 ms (350 modules)
[0] wait - compiling /_error (client and server)...
[0] error - (sc_server)/node_modules/.pnpm/[email protected]_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js (1:223) @ eval
[0] error - TypeError: e.createContext is not a function
[0] at eval (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js:11:92)
[0] at Object.(sc_server)/./node_modules/.pnpm/[email protected]_7iuvftg57tblwyxclfkwku5xo4/node_modules/next-themes/dist/index.js (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:345:1)
[0] at __webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0] at eval (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:11:69)
[0] at Object.(sc_server)/./app/[locale]/themes.tsx (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:313:1)
[0] at __webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0] at eval (webpack-internal:///(sc_server)/./app/[locale]/page.tsx:6:65)
[0] at Object.(sc_server)/./app/[locale]/page.tsx (/Users/v0/Sites/alpha/.next/server/app/[locale]/page.js:292:1)
[0] at Function.__webpack_require__ (/Users/v0/Sites/alpha/.next/server/webpack-runtime.js:33:42)
[0] at processTicksAndRejections (node:internal/process/task_queues:96:5) {
[0] type: 'TypeError',
[0] page: '/[locale]'
[0] }
That pr should be updated to work with Next.js
13.0.3
now. I'm still looking into making further improvements, but you can test the changes through the npm package @wits/next-themes prior to the pr being merged. (Or try runningnpm i "next-themes@npm:@wits/next-themes"
if you want to keep the same imports in your code.)I'd like to provide some more context on why the current approach uses cookies. Unfortunately, trying to get this working in the app directory without ensuring that the server is rendering the same
<html>
attributes results in errors. So I reached out to @pacocoursey as well as some members of the Next.js team before starting this work, and they agreed that a cookie-based approach would be the right move.
next/script
is a great call! It wasn't available for the app directory when I first started writing these changes, but it looks like using it withstategy="beforeInteractive"
works here. However, doing so inside of a client component (like the existingThemeProvider
still causes it to run too late and the page will flicker un-themed content. Also, by default, that script relies onwindow.matchMedia
to determine the system theme of the user. That isn't available on the server, so without cookies we still get errors due to a mismatch between client and server rendering during fast refreshes.

[0] event - compiled client and server successfully in 655 ms (199 modules)
[0] wait - compiling /middleware (client and server)...
[0] event - compiled successfully in 51 ms (67 modules)
[0] wait - compiling /[locale]/page (client and server)...
[0] event - compiled client and server successfully in 440 ms (362 modules)
[0] TypeError: Cannot read properties of undefined (reading '_context')
[0] at Object.useContext (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react/cjs/react.shared-subset.development.js:1418:29)
[0] at exports.useTheme (webpack-internal:///(sc_server)/./node_modules/.pnpm/@[email protected]_7iuvftg57tblwyxclfkwku5xo4/node_modules/@wits/next-themes/dist/index.js:215:28)
[0] at ChangeTheme (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:147:84)
[0] at attemptResolveElement (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1207:42)
[0] at resolveModelToJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1660:53)
[0] at Object.toJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1121:40)
[0] at stringify (<anonymous>)
[0] at processModelChunk (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:172:36)
[0] at retryTask (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1868:50)
[0] at performWork (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1906:33)
[0] TypeError: Cannot read properties of undefined (reading '_context')
[0] at Object.useContext (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react/cjs/react.shared-subset.development.js:1418:29)
[0] at exports.useTheme (webpack-internal:///(sc_server)/./node_modules/.pnpm/@[email protected]_7iuvftg57tblwyxclfkwku5xo4/node_modules/@wits/next-themes/dist/index.js:215:28)
[0] at ChangeTheme (webpack-internal:///(sc_server)/./app/[locale]/themes.tsx:147:84)
[0] at attemptResolveElement (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1207:42)
[0] at resolveModelToJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1660:53)
[0] at Object.toJSON (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1121:40)
[0] at stringify (<anonymous>)
[0] at processModelChunk (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:172:36)
[0] at retryTask (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1868:50)
[0] at performWork (webpack-internal:///(sc_server)/./node_modules/.pnpm/[email protected]_biqbaboplfbrettd7655fr4n2y/node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js:1906:33) {
[0] digest: '3476920131'
[0] }
@WITS Hi, I am using the @wits/next-themes ServerThemeProvider
, but I still get the same hydration error. Not sure what the problem might be. Here's the code snippet (it's a fresh project).
const RootLayout = ({ children }: IProps) => {
return (
<ServerThemeProvider>
<html lang="en">
<head />
<Providers>
<body className="bg-red-500 dark:bg-blue-500">{children}</body>
</Providers>
</html>
</ServerThemeProvider>
);
};
After adding attribute='class'
props to the provider the hydration still error persists, but is displayed after this error Cannot read properties of null (reading 'textContent')
. Any help would be appreciated :)
I published a new version of @wits/next-themes
(0.2.13) today that includes some fixes.
@transitive-bullshit Good catch! The "e"
value was due to a bug in my pr, and should be fixed now. However, I can still reproduce the hydration issues whenever I explicitly set the theme to something other than system. I think this might be an issue with Next caching the first value it receives for the cookie after the server starts. I'm going to investigate further and reach out to them if that's the case.
@willin Is your project using <ServerThemeProvider>
in the layout?
@dawidseipold Are you using the same properties on your <ServerThemeProvider>
as your <ThemeProvider>
? (If you're setting attribute="class"
on one, it needs to be on both.)
@WITS Yep! The hydration error is doubled if I do that. Having only <ServerThemeProvider>
gives me one error.
Okay, just published 0.2.14
which should fix more hydration errors. (Maybe all of them? 🤞)
Okay, just published
0.2.14
which should fix more hydration errors. (Maybe all of them? 🤞)
nope,
RootLayout:
<ServerThemeProvider attribute='class' themes={themes.map((x) => x.id)}>
<html lang={locale}>
<head />
<body>
<div
id='background'
className={clsx({
dark: true //darkThemes.map((x) => x.toLowerCase()).includes(theme)
})}></div>
{/* <Header /> */}
<div className='pt-20' style={{ minHeight: 'calc(100vh - 75px)' }}>
{children}
</div>
<Bootstrap />
</body>
</html>
</ServerThemeProvider>
Component:

current theme none, set theme not work.
if a nest a ThemeProvider
inside ServerThemeProvider
:

is there a working example using class
?
Hey @WITS, I can get the page to render using both <ServerThemeProvider />
and a client <ThemeProvider/>
, but I can't get setTheme to work. I'll try and reproduce it for you. In the meantime, I noticed that when using both providers, the script gets injected twice. Not sure if this would be the cause of some issues.

@willin same problem when I nest. It also shows in the console that the classes on html
element are different.
Hey @WITS, I haven't managed to get your 0.2.14 to properly function (with the ServerProvider
and ThemeProvider
), but it might be because I'm on Next 13.0.6?
Warning: Cannot render a sync or defer
I'd also like to get your thoughts on the cookie-based approach you elaborated above would play out with edge caching. If the page is requested once by a user with a "dark" cookie, we will likely get a flash of wrong theme if another requests the same cached page with a different cookie correct? Using localStorage was a way around that issue, but I guess Server Components might not leave us much of a choice. I don't think there's a way to use the Vary
cache header on one particular cookie.
Good news and bad news — although I think the bad news is exclusively for me, because I didn't realize this option existed before coding the cookie-based version.
I don't believe next-themes
actually needs any code changes to support appDir
. Using cookies opts out of SSG (and would likely lead to issues with edge caching, like @balthazar mentioned above), so I've been investigating how to solve the hydration issues without them. In doing so, I discovered React's suppressHydrationWarning property. Because layouts in the app directory render the <html>
tag, they can use this property on that element. It only applies one level deep, per the documentation, so it shouldn't block problematic hydration warnings from appearing in the console. Using this & a <Providers>
component, it should be possible to use the stable version of next-themes
in the app directory without issues.
Example:
// app/layout.jsx
import { Providers } from "./providers";
export default function Layout({ children }) {
return (
<html suppressHydrationWarning>
<head />
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// app/providers.jsx
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
In my opinion, the only thing that would still be worth changing in
next-themes
to better support this would be to prepend the'use client';
directive to the exported code. (This change would eliminate the<Providers>
requirement in the example above.) Unfortunately directives get stripped out when usingmicrobundle
, so this would either need to be done via a custom script or by switching to a different bundler.
Hi @WITS! Thanks for putting the time to try fix this.
I'm on the latest release of Next (13.1.1-canary.0). I've tried both your cookie-based version (which weirdly enough worked correctly for a couple of minutes before breaking again) and the solution you just posted, none of them work. I checked that the cookie-based version was correctly setup (same props as ThemeProvider).
Thing is, it only happens when I switch to 'light' mode and refresh the page, when it's in 'dark' it's working correctly. Maybe this is something related to my actual code, I am getting a strange error that I can't seem to find anything about it online.
Uncaught TypeError: wakeable.then is not a function at Object.markComponentSuspended (react_devtools_backend.js:5694:16) at markComponentSuspended (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:5148:30) at handleThrow (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30642:7) at renderRootSync (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30854:7) at performConcurrentWorkOnRoot (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:29978:74) at workLoop (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:3897) at flushWork (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:3606) at MessagePort.performWorkUntilDeadline (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:1787)
However, putting the workaround -or solution- posted here in Next-themes using useEffect for checking if it's mounted on the client seems to work perfectly in the RootLayout:
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted)
return (
<html>
<head />
<body></body>
</html>
);
return (
// ...the actual layout
)
Hi @JuanseChavero,
One caveat in my project was to use the exact same layout.tsx
as @WITS provided. I had some additional components inside the <body>
tag, and they cased issues. Once I moved them into the page.tsx
the hydration errors disappeared.
Maybe this helps you.
@WITS Hi, I am using the
@wits/next-themes ServerThemeProvider
, but I still get the same hydration error. Not sure what the problem might be. Here's the code snippet (it's a fresh project).const RootLayout = ({ children }: IProps) => { return ( <ServerThemeProvider> <html lang="en"> <head /> <Providers> <body className="bg-red-500 dark:bg-blue-500">{children}</body> </Providers> </html> </ServerThemeProvider> ); };
Does moving the Providers component within the body tags help?
Hi, do you have any ideas how to enable themes (dark/light) only for particular pages? Otherwise, it should be always light theme.
I was trying to do set the forcedTheme
, but it causes the page flickering, as it works on client side. Any help is appreciated, thank you!
'use client';
import { usePathname } from 'next/navigation';
import { ThemeProvider as PreferredProvider } from 'next-themes';
// eslint-disable-next-line react/prop-types
const ThemeProvider = ({ children }) => {
const pathname = usePathname();
const isDocPage = pathname.startsWith('/docs');
return (
<PreferredProvider attribute="class" forcedTheme={isDocPage ? null : 'light'}>
{children}
</PreferredProvider>
);
};
export default ThemeProvider;
Uncaught TypeError: wakeable.then is not a function at Object.markComponentSuspended (react_devtools_backend.js:5694:16) at markComponentSuspended (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:5148:30) at handleThrow (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30642:7) at renderRootSync (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30854:7) at performConcurrentWorkOnRoot (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:29978:74) at workLoop (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:3897) at flushWork (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:3606) at MessagePort.performWorkUntilDeadline (webpack-internal:///(:3000/app-client)/./node_modules/next/dist/compiled/scheduler/index.js:11:1787)
@JuanseChavero FYI this is a bug of React DevTools. If you disable React DevTools, the error would be gone.
There is already an issue opened: https://github.com/facebook/react/issues/25994
However, it appears that React team just doesn't want to work on this issue yet, so I have provided a workaround: https://github.com/facebook/react/issues/25994#issuecomment-1402287594
I managed to fix this hydration error on next 13 using app dir with the following:
import { Header } from "@/components";
import "./globals.css";
import { ThemeProvider } from "next-themes";
import { useEffect, useState } from "react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<html lang="en">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body>
{mounted && (
<ThemeProvider enableSystem={true} attribute="class">
<Header appName="" />
{children}
</ThemeProvider>
)}
</body>
</html>
);
}

@WITS any recommendation of how this could be fixed?