next-themes has 22 million weekly downloads. Last release? Over a year ago. 44 open issues. 17 unmerged pull requests. React 19 shipped, and the maintainer disappeared.
Classic open source.
The problem#
When I started migrating Hostero to Next.js 16 and React 19, it became clear pretty quickly that next-themes just doesn't work properly anymore.
The first thing that greeted me in the console:
Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.React 19 stopped tolerating <script> tags inside Client Components. next-themes does exactly that. And nobody was planning to fix it. I actually opened a pull request myself, took one look at the repo activity, and decided the same day to build it from scratch instead. That's when @wrksz/themes was born.
That's not the only problem either. With React 19 cacheComponents, themes can "freeze" on a stale value because next-themes uses plain useState instead of useSyncExternalStore. In production builds with function name minification, you get ReferenceError: __name is not defined. And so on.
I could have forked it, applied patches, published it as next-themes-maintained-fixed-... (you name it), but the entire codebase is ~300 lines. May as well do it properly from scratch.
What I built#
A drop-in replacement for next-themes. Migration is a single import change:
npm install @wrksz/themes
npm uninstall next-themes// before
import { ThemeProvider } from "next-themes";
// after
import { ThemeProvider } from "@wrksz/themes/next";Identical API. Everything else works the same.
What's fixed#
Every known bug in next-themes:
React 19 script warning - instead of rendering a <script> inside a Client Component, I use useServerInsertedHTML to inject the script outside the React tree. Zero warnings.
Stale theme with cacheComponents - useSyncExternalStore with a per-instance store instead of a global singleton. Always the current value, even when React suspends and resumes a subtree.
__name minification bug - fixed.
Multi-class themes - next-themes leaves stale classes in the DOM when switching between themes defined as value={{ dark: "dark high-contrast" }}. Fixed with flatMap + split before removing classes.
What's new#
Cookie storage with zero-flash SSR#
This is the biggest one. With storage="cookie", the provider in Next.js reads the cookie server-side automatically. The class on <html> is correct from the very first byte of HTML, no boilerplate required:
<ThemeProvider storage="cookie" defaultTheme="dark">
{children}
</ThemeProvider>No more flash on first render. No more hacks.
Generic types#
Full TypeScript safety on custom themes:
type AppTheme = "light" | "dark" | "high-contrast";
const { theme, setTheme } = useTheme<AppTheme>();Nested providers#
Each provider has an independent store. You can have different themes in different sections of the app at the same time, useful for component libraries, embeds, or isolated UI sections.
ThemedImage#
A component that solves the hydration mismatch problem for theme-dependent images:
<ThemedImage
src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
alt="Logo"
/>useThemeValue#
A utility hook for mapping theme to any value:
const label = useThemeValue({
light: "Switch to dark",
dark: "Switch to light",
});Server-side access#
getTheme() reads the current theme in Server Components, layouts, server actions, and middleware, no React dependencies needed:
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;
}Everything else#
sessionStoragesupportstorage: "none"for fully controlled themes- Meta
theme-colorfor Safari and PWA - Tailwind CSS v4 dark mode integration out of the box
Install#
npm install @wrksz/themesFull docs and migration guide at themes.wrksz.dev. If you're coming from next-themes, the migration guide covers everything.