[bug]: Theme Provider creates hydration error in Next.js 15.0.1
Describe the bug
Implementing dark mode, and putting ThemeProvider into the layout is making hydration error in newest version of Next.js (15.0.1).
Affected component/components
ThemeProvider
How to reproduce
- Do npm install next-themes
- Create
ThemeProvidercomponent. - Wrap children in layout file
Codesandbox/StackBlitz link
https://ui.shadcn.com/docs/dark-mode/next
Logs
Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used
-className="dark"
-style={{color-scheme:"dark"}}
### System Info
```bash
Next.js 15.0.1
MacOS, Google Chrome
Before submitting
- [X] I've made research efforts and searched the documentation
- [X] I've searched for existing issues
Hey, as it is marked on the doc, you should add suppressHydrationWarning on your html tag :
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: RootLayoutProps) {
return (
<>
<html lang="en" suppressHydrationWarning>
<head />
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
</>
)
}
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }
Thank you!
So this is not connected to Next.js 15? It's still that old problem we had with ThemeProvider?
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you!
So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?
Here is the real fix :
'use client'
import * as React from 'react'
const NextThemesProvider = dynamic(
() => import('next-themes').then((e) => e.ThemeProvider),
{
ssr: false,
}
)
import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
You have to dynamically import the theme provider from next-theme and you can now remove the suppressHydrationWarning from your <html> tag
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you! So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?Here is the real fix :
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }You have to dynamically import the theme provider from
next-themeand you can now remove thesuppressHydrationWarningfrom your<html>tag
Amazing! That's exactly what I needed!
Thank you! You're a true web dev warrior!
This solution is effective in preventing errors during SSR, but it isn’t an "elegant" approach. Dynamically loading the next-themes library can lead to undesired outcomes, such as a "snapshot of the wrong theme" at the start.
This solution is effective in preventing errors during SSR, but it isn’t an "elegant" approach. Dynamically loading the next-themes library can lead to undesired outcomes, such as a "snapshot of the wrong theme" at the start.
That means that there is somewhere an issue, either with next-theme or with NextJs.
Now I think, I preffer having a "snapshot of the wrong theme" instead of suppressing all my hydration warning and troubleshouting during hours because nobody told me that i have put a <div> into an <p> in development mode.
But for the build, you're right, you could just suppress hydration warnings by using env like :
<html suppressHydrationWarning={process.env.NODE_ENV === 'production'}>
<>{children}</>
</html>
And then in your theme-provider.tsx :
'use client'
import * as React from 'react'
import dynamic from 'next/dynamic'
import { type ThemeProviderProps } from 'next-themes/dist/types'
import { ThemeProvider as StaticProvider } from 'next-themes'
const DynProvider = dynamic(
() => import('next-themes').then((e) => e.ThemeProvider),
{
ssr: false,
}
)
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const NextThemeProvider =
process.env.NODE_ENV === 'production' ? StaticProvider : DynProvider
return <NextThemeProvider {...props}>{children}</NextThemeProvider>
}
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }
Thanks! It seems to be the correct solution in general, although I must clarify that something different has worked better for me, understanding that it slows down rendering, in small projects I have not noticed any difference. On the other hand, using dynamic with ssr: false has generated a small flash at the start that I prefer to avoid. I am quite new to this, I am probably making mistakes, so I would like to read them.
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import { useEffect, useState } from "react";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setIsLoaded(true);
}, []);
if (!isLoaded) {
return null;
}
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
'use client'
import * as React from 'react'
const NextThemesProvider = dynamic(
() => import('next-themes').then((e) => e.ThemeProvider),
{
ssr: false,
}
)
import { type ThemeProviderProps } from 'next-themes/dist/types'
import dynamic from 'next/dynamic'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
i tried that it works fine, but when i am using next intl, with this fix, i got non sense bug. assume i have 2 languages (en, de) if i have "de" selected and refresh the page it's fine, but if i switch to "en" and switch back to "de" i only see blank white screen. this not happening if i refresh the page with the default language in my case "en". my best guess next intl is changing the html props to add language like this
const { locale } = await params;
return (
<html lang={locale}>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<MainProvider>{children}</MainProvider>
</body>
</html>
);
and next themes is changing over it, or vise versa. that introduce this conflict, i checked the terminal and the pages was requested exactly as it supposed to no errors on console, no errors on terminal
if (!isLoaded) { return null; }
that's not a good solution either. That useEffect will not run until after the component is mounted and painted, meaning you will initially see a blank screen for hundred or so milliseconds before NextThemesProvider can run. It's not an ideal. The whole point of SSR is to avoid this type of rendering delay. According to next-theme's readme the suppressHydrationWarning prop on the html tag is mostly innocuous because
"This property [
suppressHydrationWarning] only applies one level deep, so it won't block hydration warnings on other elements."
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }
This worked for me in Next.js 15
The suppressHydrationWarning won't work for deeper server-client differences like a Theme switcher:
<div className="flex flex-col items-center space-y-2">
<Label>Theme</Label>
<RadioGroup
value={theme}
onValueChange={setTheme}
className="flex justify-center gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dark" id="dark" />
<Label htmlFor="dark">Dark</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="light" id="light" />
<Label htmlFor="light">Light</Label>
</div>
</RadioGroup>
</div>
Disabling ssr will lead to a flash which is not a fix but rather a reshaped issue.
Does anyone know why the current theme is not available when ssr?
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you!
So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?
yeah this solves my problem
I have the same issue on Next Js 14
I have the same issue on Next Js 14
Have you tried the suggestion provided by @Medaillek ?
okay for me there was dependency issue for farmer-motion with next js 15 - react 19
├─┬ framer-motion 11.11.13
│ ├── ✕ unmet peer react@^18.0.0: found 19.0.0-rc-66855b96-20241106
│ └── ✕ unmet peer react-dom@^18.0.0: found 19.0.0-rc-66855b96-20241106
└─┬ next-themes 0.3.0
├── ✕ unmet peer react@"^16.8 || ^17 || ^18": found 19.0.0-rc-66855b96-20241106
└── ✕ unmet peer react-dom@"^16.8 || ^17 || ^18": found 19.0.0-rc-66855b96-20241106
also weirdly could not find the ThemeProviderProps
import { type ThemeProviderProps } from "next-themes/dist/types";
So, I downgrade "next-themes": "^0.4.3", to "next-themes": "^0.3.0",
no hydration error now !!! happy 😁
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you! So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?Here is the real fix :
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }You have to dynamically import the theme provider from
next-themeand you can now remove thesuppressHydrationWarningfrom your<html>tag
It did fix the hydration error for me but it caused this:
--><template
data-dgst="BAILOUT_TO_CLIENT_SIDE_RENDERING"
data-msg="Switched to client rendering because the server rendering errored:
Bail out to client-side rendering: next/dynamic"
data-stck="Switched to client rendering because the server rendering errored:
Error: Bail out to client-side rendering: next/dynamic
at BailoutToCSR (webpack-internal:///(ssr)/./node_modules/next/dist/shared/lib/lazy-dynamic/dynamic-bailout-to-csr.js:15:15)
at react-stack-bottom-frame (
Not sure how will this affect the SEO of my page?
For anyone who doesn’t want to scroll through all the text above:
Here’s what I’ve found and confirmed—among all the solutions, adding suppressHydrationWarning to your HTML tag is the best solution for now!
Solution 1:
As it is marked on the next-themes Github Readme, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }This property only applies one level deep, so it won't block hydration warnings on other elements.
Solution 2:
Less effective Solution: You need to dynamically import the
ThemeProviderfromnext-theme, which allows you to remove thesuppressHydrationWarningfrom your tag.
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }This solution effectively prevents errors during SSR but isn’t an "elegant" approach; but dynamically loading the next-themes library can result in undesired outcomes, such as displaying a "snapshot of the wrong theme" initially.
Verdict: It’s best to stick with Solution 1 for now until a proper solution is released.
With solution 1, I'm getting this errors
Cannot find module '@/components/theme-provider' or its corresponding type declarations.ts(2307)
Cannot find name 'RootLayoutProps'.ts(2304)
Any suggestions???
Thanks
import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }
@birarr follow this guide, you probably don't have the theme-provider file.
@birarr follow this guide, you probably don't have the theme-provider file.
Actually, I do have this on my project already. But I was able to fix it now, adding the "suppressHydrationWarning" on my html tag.
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you! So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?Here is the real fix :
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }You have to dynamically import the theme provider from
next-themeand you can now remove thesuppressHydrationWarningfrom your<html>tag
I was facing this problem too and this works well. Thank you @SyahmiRafsan
Hey, as it is marked on the doc, you should add
suppressHydrationWarningon your html tag :import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: RootLayoutProps) { return ( <> <html lang="en" suppressHydrationWarning> <head /> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> </body> </html> </> ) }Thank you! So this is not connected to Next.js 15? It's still that old problem we had with
ThemeProvider?Here is the real fix :
'use client' import * as React from 'react' const NextThemesProvider = dynamic( () => import('next-themes').then((e) => e.ThemeProvider), { ssr: false, } ) import { type ThemeProviderProps } from 'next-themes/dist/types' import dynamic from 'next/dynamic' export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }You have to dynamically import the theme provider from
next-themeand you can now remove thesuppressHydrationWarningfrom your<html>tagI was facing this problem too and this works well. Thank you @SyahmiRafsan
But it blinking on reload the page...(
Still facing with this issue. Any one have better solutions?
suppressHydrationWarning is work but I think this is not a real fix.
adding dynamic logic in themeProvider is also working but make another issue.
:(
just for information:
chakra-ui suggest add suppressHydrationWarning option at html tag in doc.
https://www.chakra-ui.com/docs/get-started/frameworks/next-app
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"next": "15.1.2",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19.0.2",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.2",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
I'm getting the same error too. I am looking for a real solution, I think I will continue with the "suppressHydrationWarning" solution for now. If anyone has a better solution, you would be happy to see the answer.
I've been sitting with this problem for two days. The solution to add suppressHydrationWarning to html helped. Thanks guys. I didn't know what to do anymore
hi yall
for some reason suppressHydraitonWarning isn't working for me..
import { ThemeProvider } from "@/components/theme-provider";]
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<TonConnectProvider>
<body className='flex flex-col min-h-[99vh] antialiased'>
<main className="flex-grow">
{children}
</main>
</body>
</TonConnectProvider>
</ThemeProvider>
</html>
);
}
'use client';
import { ThemeProvider as NextThemesProvider } from "next-themes";
import React from "react";
export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
any thoughts?
Im trying to avoid disabling ssr, cause it makes a little flashbang on load, and thats not what i really want to see.
I would be grateful for any help.
https://github.com/shadcn-ui/ui/issues/5552#issuecomment-2437836976
@jus1d, you might want to give that a try. It worked for me like a charm. good luck
@rafadlis thanks, but dynamic imports creating a new issue with a little flash bang on load, which is very annoying.
Though, suppressHydrationError will not work in this case, because it suppress only text difference errors or something like that. I personally wrote a little context with my own hooks and that works perfect
Thank you guys it worked with adding suppressHydrationError on the HTML tag
None of the solutions discussed here have worked for me. I'm using next: 15.1.3 , react: ^19.0.0 , node: v22.12.0
<ClientSegmentRoot Component={function DashboardLayout} slots={{...}} params={{}}>
<DashboardLayout params={Promise}>
<html lang="en" suppressHydrationWarning={true}>
<head>
<body
>
...
...
className="dark"
style={{color-scheme:"dark"}}
data-new-gr-c-s-check-loaded="14.1092.0"
data-gr-ext-installed=""
className="geist_6feb203d-module__8DQF1a__variable geist_mono_c7d183a-module__ZW1U..."
data-new-gr-c-s-check-loaded="14.1092.0"
data-gr-ext-installed=""