What happened?
What happened?
When the ThemeProvider subtree remounts on the client, React 19.2 logs the following console error in development:
Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client. Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
at script (<anonymous>:null:null)
at ThemeProvider (...next-themes/dist/index.mjs)
at LocaleLayout (app/[lang]/layout.tsx:185)
Steps to reproduce
-
Create a Next.js App Router app (Next.js 16, React 19.2.x) with the root layout inside a dynamic segment — e.g. app/[lang]/layout.tsx as required by next-intl's locale-prefixed routing.
-
Wrap the app in ThemeProvider inside that layout:
// app/[lang]/layout.tsx
<html lang={locale}>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
-
Switch locales with a client-side navigation that changes the root segment value, e.g. next-intl's useRouter().push(pathname, { locale }).
-
Observe the browser console in dev mode — the error above appears immediately after the remount.
Root cause analysis
I dug into this and verified each step in the browser:
-
Changing a root segment value makes Next.js remount the entire root-layout subtree (not just re-render it). I confirmed this by tagging DOM nodes with expando properties before the navigation: every <script> element inside the ThemeProvider subtree was re-created after the locale switch, while a normal same-locale navigation remounts nothing.
-
next-themes renders its no-flash inline <script> inside the provider via React.createElement("script", { dangerouslySetInnerHTML: ... }) (the memoized ThemeScript component in dist/index.mjs).
-
React 19.2's react-dom client renderer now explicitly warns when a client-render-created <script> is not a data block (checked in completeWork: the element is created through a dummy <div> + innerHTML trick, i.e. it is intentionally inert, and console.error fires unless isScriptDataBlock(props) is true — only non-executable types like application/ld+json are exempt; the async-script hoisting path only applies to scripts with a string src, so an inline script can't opt out).
-
The re-created script never executes — but that's fine, because its only job (applying the theme class before first paint) already ran when the initial SSR HTML loaded, and the provider's useEffect re-applies the theme on remount. So the error is noisy but harmless: theming keeps working. It's purely a DX problem.
Any scenario that remounts the provider on the client should hit this (root segment change is just the most common one with i18n routing).
Expected behavior
No error. The no-flash script only needs to render for SSR + the initial hydration mount. On subsequent client-side remounts it should be skipped entirely.
Suggested fix
Render the script only for the first mount, using a module-level flag flipped in an effect:
let hasMounted = false
const ThemeScript = memo((props) => {
const contents = ...
useEffect(() => {
hasMounted = true
}, [])
// On client re-mounts (after first hydration), skip the script: its job is
// already done and React 19.2 errors on client-rendered script tags.
return hasMounted ? null : createElement('script', {...})
})
- During SSR and the first hydration mount,
hasMounted is false, so the script renders exactly as today (the useEffect only runs after hydration).
- Any later client remount renders
null — no inert script element, no React warning, identical behavior (theme re-application already happens in the provider's effects).
I've verified this exact patch locally against 0.4.6: the error disappears on locale switches, SSR output is unchanged, and theme toggling/persistence keeps working after remounts. Happy to open a PR if the approach sounds good.
Version
next-themes 0.4.6 (latest) — also relevant: next 16.3.0, react/react-dom 19.2.8
What browsers are you seeing the problem on?
Chrome, Firefox, Microsoft Edge, Safari, other (the error is emitted by React's client renderer in dev mode, so it is browser-independent)
Version
0.4.6
What browsers are you seeing the problem on?
Firefox, Chrome, Safari, Microsoft Edge
What happened?
What happened?
When the
ThemeProvidersubtree remounts on the client, React 19.2 logs the following console error in development:Steps to reproduce
Create a Next.js App Router app (Next.js 16, React 19.2.x) with the root layout inside a dynamic segment — e.g.
app/[lang]/layout.tsxas required bynext-intl's locale-prefixed routing.Wrap the app in
ThemeProviderinside that layout:Switch locales with a client-side navigation that changes the root segment value, e.g. next-intl's
useRouter().push(pathname, { locale }).Observe the browser console in dev mode — the error above appears immediately after the remount.
Root cause analysis
I dug into this and verified each step in the browser:
Changing a root segment value makes Next.js remount the entire root-layout subtree (not just re-render it). I confirmed this by tagging DOM nodes with expando properties before the navigation: every
<script>element inside theThemeProvidersubtree was re-created after the locale switch, while a normal same-locale navigation remounts nothing.next-themesrenders its no-flash inline<script>inside the provider viaReact.createElement("script", { dangerouslySetInnerHTML: ... })(the memoizedThemeScriptcomponent indist/index.mjs).React 19.2's
react-domclient renderer now explicitly warns when a client-render-created<script>is not a data block (checked incompleteWork: the element is created through a dummy<div>+innerHTMLtrick, i.e. it is intentionally inert, andconsole.errorfires unlessisScriptDataBlock(props)is true — only non-executable types likeapplication/ld+jsonare exempt; the async-script hoisting path only applies to scripts with a stringsrc, so an inline script can't opt out).The re-created script never executes — but that's fine, because its only job (applying the theme class before first paint) already ran when the initial SSR HTML loaded, and the provider's
useEffectre-applies the theme on remount. So the error is noisy but harmless: theming keeps working. It's purely a DX problem.Any scenario that remounts the provider on the client should hit this (root segment change is just the most common one with i18n routing).
Expected behavior
No error. The no-flash script only needs to render for SSR + the initial hydration mount. On subsequent client-side remounts it should be skipped entirely.
Suggested fix
Render the script only for the first mount, using a module-level flag flipped in an effect:
hasMountedisfalse, so the script renders exactly as today (theuseEffectonly runs after hydration).null— no inert script element, no React warning, identical behavior (theme re-application already happens in the provider's effects).I've verified this exact patch locally against 0.4.6: the error disappears on locale switches, SSR output is unchanged, and theme toggling/persistence keeps working after remounts. Happy to open a PR if the approach sounds good.
Version
next-themes 0.4.6 (latest) — also relevant: next 16.3.0, react/react-dom 19.2.8
What browsers are you seeing the problem on?
Chrome, Firefox, Microsoft Edge, Safari, other (the error is emitted by React's client renderer in dev mode, so it is browser-independent)
Version
0.4.6
What browsers are you seeing the problem on?
Firefox, Chrome, Safari, Microsoft Edge