DEV Community

Cover image for I was tired of next-themes being abandoned, so I built a drop-in replacement
Jakub
Jakub

Posted on

I was tired of next-themes being abandoned, so I built a drop-in replacement

next-themes has 22 million weekly downloads. It hasn't had a release in over a year. There are 43 open issues and 16 unmerged PRs sitting there, including React 19 compatibility bugs that affect every project upgrading to Next.js 16.

I got tired of waiting. So I built @wrksz/themes.

The problem with next-themes

When Next.js 16 and React 19 dropped, projects started hitting a wall. The most common issues:

React 19 script warning - next-themes renders an inline <script> inside a Client Component. React 19 started warning about this explicitly:

Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.
Enter fullscreen mode Exit fullscreen mode

Stale theme with cacheComponents - next-themes uses useState + useContext, which causes stale theme values when React's Activity or cacheComponents suspend and resume a subtree. Your theme gets stuck.

__name minification bug - production builds that minify function names break next-themes internals silently.

Multi-class themes leave stale classes - if you map a theme to multiple CSS classes like "dark high-contrast", switching away from it leaves high-contrast stuck on the DOM forever.

All of these have been sitting open for months with no movement.

What I built

@wrksz/themes is a drop-in replacement. Migrating is literally one import change:

npm uninstall next-themes
npm install @wrksz/themes
Enter fullscreen mode Exit fullscreen mode
// before
import { ThemeProvider } from "next-themes";

// after
import { ThemeProvider } from "@wrksz/themes/next";
Enter fullscreen mode Exit fullscreen mode

That's it. The API is identical.

What's fixed

Every known bug from next-themes is fixed:

  • React 19 script warning - fixed by using useServerInsertedHTML to inject the script outside the React component tree
  • Stale theme with cacheComponents - fixed by using useSyncExternalStore with a per-instance store
  • __name minification bug - fixed
  • Multi-class theme removal - fixed with proper flatMap + split before removing classes

What's new

Beyond fixes, I added features that were frequently requested but never implemented:

Cookie storage with zero-flash SSR - the biggest one. With storage="cookie", the Next.js provider reads the cookie server-side automatically. No flash, no suppressHydrationWarning hacks, no boilerplate:

// app/layout.tsx
import { ThemeProvider } from "@wrksz/themes/next";

export default async function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider storage="cookie" defaultTheme="dark">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Generic types - full TypeScript safety with your custom theme names:

type AppTheme = "light" | "dark" | "high-contrast";

const { theme, setTheme } = useTheme<AppTheme>();
// theme: AppTheme | "system" | undefined
// setTheme: (theme: AppTheme | "system") => void
Enter fullscreen mode Exit fullscreen mode

Nested providers - each provider gets its own independent store. You can scope themes to specific sections of your app:

<ThemeProvider forcedTheme="dark" target="#landing" storage="none">
  <div id="landing">...</div>
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

ThemedImage - a built-in component that solves the hydration mismatch problem with theme-aware images:

import { ThemedImage } from "@wrksz/themes/client";

<ThemedImage
  src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
  alt="Logo"
  width={200}
  height={50}
/>
Enter fullscreen mode Exit fullscreen mode

useThemeValue - returns a value from a map based on the current theme:

const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
Enter fullscreen mode Exit fullscreen mode

sessionStorage, storage: "none" - more storage options for different use cases.

meta theme-color - updates <meta name="theme-color"> automatically for Safari and PWAs.

Comparison table

next-themes @wrksz/themes
React 19 script warning Yes Fixed
__name minification bug Yes Fixed
Stale theme with cacheComponents Yes Fixed
Multi-class removal bug Yes Fixed
Nested providers No Yes
sessionStorage support No Yes
Cookie storage (zero-flash SSR) No Yes
Disable storage No Yes
meta theme-color No Yes
Server-provided theme No Yes
Generic TypeScript types No Yes
Zero runtime dependencies Yes Yes

Try it

bun add @wrksz/themes
# or
npm install @wrksz/themes
Enter fullscreen mode Exit fullscreen mode

Docs: themes.wrksz.dev
GitHub: github.com/jakubwarkusz/themes

Migration guide: themes.wrksz.dev/docs/migration

Top comments (0)