@wrksz/themesv2.0.0-beta.1 · beta

Migrating from next-themes

Step-by-step guide for migrating from next-themes to @wrksz/themes.

Edit on GitHub

Last updated on

@wrksz/themes is a near drop-in replacement for next-themes. For most apps, migration is a single import change. This page covers that change and any behavioral differences to be aware of.

Step 1: Install

Current docs target 2.0.0-beta.1. Use @wrksz/themes@beta. For stable 1.x, install @wrksz/themes@1.2.0 and skip Upgrading from 1.x.

bun add @wrksz/themes@beta
npm install @wrksz/themes@beta
pnpm add @wrksz/themes@beta
yarn add @wrksz/themes@beta

Step 2: Update the provider import

- import { ThemeProvider } from "next-themes";
+ import { ThemeProvider } from "@wrksz/themes/next";

That's it for most apps.

Upgrading from 1.x

Breaking · v2.0.0-beta.1 Breaking changes below ship in 2.0.0-beta.1 (npm install @wrksz/themes@beta). npm latest is still 1.2.0. Skip this section if you stay on 1.x.

What broke vs 1.2.0

Change1.x2.0.0-beta.1
Next ThemeProviderasync, called cookies() for storage="cookie"|"hybrid" and set initialTheme for youSync passthrough. No request-time cookie read.
SSR <html className={theme}>Often came "for free" from the providerYou must call getTheme and pass initialTheme (and usually export const instant = false)
TypeScript peer>=4.5.0>=5.9 (5.0–5.8 unsupported)
forcedThemeCould write the forced value into storage on initDoes not persist to storage
initialTheme / storageKeyProp changes could re-run mount initMount init is sticky; later prop changes do not re-initialize

Migration path

  • For zero-flash without request-time APIs, keep storage="cookie" or "hybrid". The pre-paint bootstrap still reads the cookie before hydration.
  • If server-rendered markup must know the theme, do this explicitly:
app/layout.tsx
import { ThemeProvider, getTheme } from "@wrksz/themes/next";

export const instant = false;

export default async function RootLayout({ children }) {
  const theme = await getTheme();

  return (
    <html className={theme} suppressHydrationWarning>
      <body>
        <ThemeProvider storage="cookie" initialTheme={theme}>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Step 3: Remove the "use client" wrapper

@wrksz/themes/next is the server-layout entry and also exports the server-only getTheme helper. If you have a client wrapper (common in Next.js next-themes setups), remove it.

Breaking · v2.0.0-beta.1 In 1.x this entry was an async Server Component. From 2.0.0-beta.1 it is synchronous, but it still must not be imported from a "use client" module because it re-exports getTheme.

- "use client";
- import { ThemeProvider } from "next-themes";
- export function Providers({ children }) {
-   return <ThemeProvider>{children}</ThemeProvider>;
- }
app/layout.tsx
+ import { ThemeProvider } from "@wrksz/themes/next";
  export default function RootLayout({ children }) {
    return (
      <html suppressHydrationWarning>
        <body>
+         <ThemeProvider>{children}</ThemeProvider>
        </body>
      </html>
    );
  }

If you need a nested provider inside a Client Component, use ClientThemeProvider instead.

Step 4: Update client hook imports

- import { useTheme } from "next-themes";
+ import { useTheme } from "@wrksz/themes/client";

API differences

All next-themes props are supported. The following props have changed behavior or are new:

onThemeChange

In next-themes, onThemeChange fires with the resolved theme (e.g. "dark") even when setTheme("system") is called.

In @wrksz/themes, onThemeChange fires with the selected value - which may be "system". System preference changes while the theme is "system" still fire with the resolved value.

// next-themes: setTheme("system") fires onThemeChange("dark")
// @wrksz/themes: setTheme("system") fires onThemeChange("system")

If you are persisting the theme server-side via onThemeChange, handle "system" explicitly:

<ThemeProvider
  onThemeChange={(theme) => {
    if (theme !== "system") saveTheme(theme);
  }}
>

disableTransitionOnChange

Accepts boolean | string. Passing a CSS transition string suppresses only those specific properties, keeping other transitions intact.

// next-themes
<ThemeProvider disableTransitionOnChange>

// @wrksz/themes - also accepts a CSS string
<ThemeProvider disableTransitionOnChange="background-color 0s, color 0s">

New features

After migrating, these features are available without any additional configuration:

  • storage="hybrid" - cookie-first reads for SSR + localStorage mirror for cross-tab sync.
  • storage="cookie" - zero-flash SSR without boilerplate. Pass storage="cookie" to ThemeProvider from @wrksz/themes/next.
  • storage="sessionStorage" - persists theme only for the current tab.
  • storage="none" - no persistence, useful for scoped or forced themes.
  • themeColor - updates <meta name="theme-color"> on theme change.
  • initialTheme - initialize theme from a server-side source.
  • followSystem - always follow system preference, ignoring stored value.
  • getTheme() - read the current theme from a cookie outside React.
  • createThemes(...) - typed factory for provider + hooks from one canonical theme tuple.
  • useThemeEffect(...) - side effects that run on theme changes after mount.

See Why not next-themes? for the full comparison.

On this page