@wrksz/themes - why I rewrote next-themes from scratch

next-themes has 22 million weekly downloads and hasn't had a release in over a year. I built a drop-in replacement that fixes every known bug and adds the features that were always missing.

·3 min read

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:

code
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:

bash
npm install @wrksz/themes
npm uninstall next-themes
tsx
// 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#

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:

tsx
<ThemeProvider storage="cookie" defaultTheme="dark">
  {children}
</ThemeProvider>

No more flash on first render. No more hacks.

Generic types#

Full TypeScript safety on custom themes:

tsx
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:

tsx
<ThemedImage
    src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
    alt="Logo"
/>

useThemeValue#

A utility hook for mapping theme to any value:

tsx
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:

tsx
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#

  • sessionStorage support
  • storage: "none" for fully controlled themes
  • Meta theme-color for Safari and PWA
  • Tailwind CSS v4 dark mode integration out of the box

Install#

bash
npm install @wrksz/themes

Full docs and migration guide at themes.wrksz.dev. If you're coming from next-themes, the migration guide covers everything.