A faithful port of next-themes 0.4.6
(MIT, © Paco Coursey) with one behavioral change: the anti-flash theme-init
script is emitted as raw HTML through a hidden <div dangerouslySetInnerHTML>
instead of a JSX <script> element.
React refuses to mount <script> elements during client rendering — it logs
Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client.
and silently drops the tag from the DOM. Upstream's inline <script> works
only while the ThemeProvider never remounts. Any client-side remount
triggers the error and loses the script — typical cases:
- a root layout inside a dynamic segment (e.g.
app/[locale]/layout.js), remounted by a client-side locale switch; - Fast Refresh remounts during development.
Scripts inside innerHTML are inert, which is exactly the behavior wanted
here: the browser executes the script while parsing the server-rendered
document (so there is no flash of the wrong theme), and on a client remount it
is not re-executed — the provider's effects re-apply the theme instead.
No build step, no dependencies — installable straight from a git URL:
npm install github:<owner>/next-themes-inert
# or, pinned to a tag/commit (recommended):
npm install github:<owner>/next-themes-inert#v1.0.0Drop-in for the next-themes API surface listed below:
import { ThemeProvider, useTheme } from "next-themes-inert";Because the script wrapper is a <div>, render the provider inside
<body>, not <head> (that is where it normally lives anyway):
export default function RootLayout({ children }) {
return (
<html suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}useTheme() returns { theme, setTheme, resolvedTheme, themes, systemTheme, forcedTheme } with upstream semantics (theme is undefined until
hydration). ThemeProvider accepts upstream's props: themes, defaultTheme,
attribute (default "data-theme"), value, forcedTheme, enableSystem,
enableColorScheme, disableTransitionOnChange, storageKey, nonce.
- The theme-init script is injected inertly (see Why). Everything about when it runs on a full page load is unchanged.
- The undocumented
scriptPropsprop is not supported. - A
nonceis rendered on both server and client (upstream blanks it on the client to dodge a hydration quirk that the inert wrapper doesn't have). - Ships readable, unminified ES module source; no CJS build.
- Zero runtime dependencies; the only peer is
react >= 18. - No network access, no storage beyond
localStorage[storageKey]. ThemeProviderconfig values are serialized into an inline script. They are meant to be compile-time constants in your layout — do not feed them from user input.
The source is one file (index.js), hand-ported from upstream's published
dist. To audit against upstream:
npm pack next-themes@0.4.6 && tar xzf next-themes-0.4.6.tgz
# compare package/dist/index.mjs with index.jsIf upstream ships a fix you need, port it and bump the version tag.