By March 2026, next-themes had not shipped a release in a year (0.4.6, March 2025). The numbers at that point:
- weekly downloads
- 22M
- open issues
- 44
- pull requests waiting for review
- 17
Nobody was merging anything, and modern setups had started to break.
What broke under React 19
During a migration of Hostero to Next.js 16 and React 19, next-themes started logging this in the console:
Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.I opened a pull request, looked at the repository activity, and decided a proper rewrite of a 300-line library made more sense than maintaining a fork. The warning also turned out to be one of four problems:
| Symptom | Cause in next-themes | Fix in @wrksz/themes |
|---|---|---|
<script> warning on every render | the blocking script renders inside a Client Component | injected with useServerInsertedHTML, outside the component tree |
theme stuck on a stale value under cacheComponents | state lives in useState | a store per provider, read through useSyncExternalStore |
ReferenceError: __name is not defined in some production builds | the inline script is built with Function.toString(), so a bundler's __name helper ends up in code that runs without it | the bootstrap ships as a prebuilt string |
InvalidCharacterError with value={{ dark: "dark high-contrast" }} | the whole value goes to classList as a single token | classes are split with flatMap before they are added or removed |
Drop-in replacement
@wrksz/themes keeps the same props and hooks, so switching is mostly an install and new imports:
npm install @wrksz/themes
npm uninstall next-themespnpm add @wrksz/themes
pnpm remove next-themesbun add @wrksz/themes
bun remove next-themesimport { ThemeProvider } from "next-themes";
import { useTheme } from "next-themes";
import { ThemeProvider } from "@wrksz/themes/next";
import { useTheme } from "@wrksz/themes/client"; The provider import goes in the server layout, the hooks in client components. One default differs: next-themes sets data-theme, @wrksz/themes sets a class, so pass attribute="data-theme" if your CSS relies on the old behavior.
What it adds
Cookie storage without a flash
With storage="cookie", the theme lives in a cookie and the provider's bootstrap script reads it synchronously before the first paint, so the page never flashes the wrong theme. Nothing reads cookies on the server, so the layout can stay static:
<ThemeProvider storage="cookie" defaultTheme="dark">
{children}
</ThemeProvider>Typed themes
Pass your own union and setTheme rejects anything outside it at compile time:
import { useTheme } from "@wrksz/themes/client";
type AppTheme = "light" | "dark" | "high-contrast";
const { theme, setTheme } = useTheme<AppTheme>();
setTheme("sepia");
Nested providers
Each provider has its own store, so two parts of one page can run different themes at the same time, as long as each gets its own target and storageKey. That helps component libraries, embeds and isolated previews.
ThemedImage and useThemeValue
ThemedImage picks the image for the current theme without a hydration mismatch:
import { ThemedImage } from "@wrksz/themes/client";
<ThemedImage
src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
alt="Logo"
/>useThemeValue does the same for any value:
import { useThemeValue } from "@wrksz/themes/client";
const label = useThemeValue({
light: "Switch to dark",
dark: "Switch to light",
});Server-side access
getTheme() reads the theme cookie without pulling in React. In a proxy or middleware, pass the request:
import { NextResponse } from "next/server";
import { getTheme } from "@wrksz/themes/next";
export function proxy(request: Request) {
const theme = getTheme(request, { defaultTheme: "dark" });
const response = NextResponse.next();
response.headers.set("x-theme", theme);
return response;
}In a Server Component, layout or server action, call it without the request and await it: await getTheme({ defaultTheme: "dark" }). Reading the cookie makes that render request-time, and the result can be "system", so resolve it before using it as a class.
Smaller additions
sessionStoragesupportstorage: "none"for fully controlled themes- meta
theme-colorfor Safari and PWAs hybridstorage: a cookie plus cross-tab sync throughlocalStorage
Since then
The post was written against 0.7.9. Here is how the project got to where it is now:
Pull request to next-themes
A fix for the React 19 script warning. Still open.0.1.0 on npm
Eight releases that first day, up to 0.5.0.This post
Written against 0.7.9.0.9.0
Hybrid storage, thecreateThemesfactory anduseThemeEffect.1.0.0
A 46% smaller npm tarball after splitting the type declarations.1.1.0
The first outside contributor, @martijn00, with portable SSR and client APIs.2.0.0
No more implicit cookie reads on the server under Next.js 16.3, and TypeScript 5.9 required.
Documentation
The full API is at themes.wrksz.dev. Coming from next-themes, start with the migration guide.