@wrksz/themesv2.1.1
Examples

Hybrid storage

Use cookies for SSR and localStorage for cross-tab sync.

storage="hybrid" stores the theme in a cookie and mirrors it to localStorage. The pre-paint bootstrap reads the cookie before the first paint. The Next does not call cookies(). localStorage keeps other tabs in sync.

Try it

Switch the theme and watch both stores update: the cookie is what the bootstrap reads before paint, localStorage carries the change to other tabs. This demo uses its own key, so it never touches the page theme.

cookie …localStorage …

Hedy Lamarr

Cookie first, mirrored to localStorage

Open this page in a second tab and switch here: the other tab follows.

OpenLater
app/layout.tsx
<ThemeProvider storage="hybrid" storageKey="theme">
  {children}
</ThemeProvider>

Root layout

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          storage="hybrid"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Use @wrksz/themes/next in App Router layouts so the anti-flash script is injected through useServerInsertedHTML.

Theme toggle

app/theme-toggle.tsx
"use client";

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

export function ThemeToggle() {
  const { theme, resolvedTheme, setTheme } = useTheme();

  return (
    <div>
      <p>
        Selected: {theme}
        <br />
        Resolved: {resolvedTheme}
      </p>
      <button type="button" onClick={() => setTheme("light")}>
        Light
      </button>
      <button type="button" onClick={() => setTheme("dark")}>
        Dark
      </button>
      <button type="button" onClick={() => setTheme("system")}>
        System
      </button>
    </div>
  );
}

Open the same app in two tabs, switch the theme in one tab, and the other tab updates through the storage event.

CSS

app/globals.css
:root {
  --bg: #ffffff;
  --fg: #0a0a0a;
}

:root.dark {
  --bg: #0a0a0a;
  --fg: #fafafa;
}

body {
  background: var(--bg);
  color: var(--fg);
}

Avoid mixing this with a broad @media (prefers-color-scheme: dark) fallback for the same tokens. The provider sets the correct class before hydration.