npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wrksz/themes

v0.7.7

Published

A modern, fully-featured theme management library for Next.js

Downloads

2,571

Readme

@wrksz/themes

npm version docs

Modern theme management for Next.js 16+ and React 19+. Near drop-in replacement for next-themes - fixes every known bug and adds missing features. Migrating requires changing one import line.

bun add @wrksz/themes
# or
npm install @wrksz/themes

Why not next-themes?

| | next-themes | @wrksz/themes | |---|:---:|:---:| | React 19 script warning | ❌ | ✅ useServerInsertedHTML | | __name minification bug | ❌ | ✅ | | Stale theme with React 19 cacheComponents | ❌ | ✅ useSyncExternalStore | | Multi-class theme removal leaving stale classes | ❌ | ✅ | | Nested providers | ❌ | ✅ per-instance store | | sessionStorage support | ❌ | ✅ | | cookie storage (zero-flash SSR) | ❌ | ✅ | | Disable storage | ❌ | ✅ storage="none" | | meta theme-color support | ❌ | ✅ themeColor prop | | Server-provided theme | ❌ | ✅ initialTheme prop | | disableTransitionOnChange per property | ❌ | ✅ pass a CSS string | | Read theme outside React | ❌ | ✅ getTheme() helper | | Generic types | ❌ | ✅ useTheme<AppTheme>() | | Zero runtime dependencies | ✅ | ✅ |

Table of Contents

Setup

Add the provider to your root layout. Import from @wrksz/themes/next for Next.js - this avoids the React 19 inline script warning by using useServerInsertedHTML. Add suppressHydrationWarning to <html> to prevent hydration mismatches.

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

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

Note: ThemeProvider from @wrksz/themes/next is an async Server Component. Use it directly in layout.tsx - it cannot be wrapped in a "use client" component. For nested providers inside Client Components, use ClientThemeProvider.

Usage

"use client";

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

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

  return (
    <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
      Toggle theme
    </button>
  );
}

Zero-flash SSR with cookie storage

Use storage="cookie" with @wrksz/themes/next to eliminate SSR theme flash. The provider reads the cookie server-side automatically - no boilerplate required:

// 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="cookie" defaultTheme="dark" disableTransitionOnChange>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

For apps using CSS media queries (@media (prefers-color-scheme: dark)) alongside CSS class variables, avoid the media query fallback - the library sets the correct class before the first paint:

/* ❌ causes flash when system pref differs from stored theme */
@media (prefers-color-scheme: dark) {
  :root:not(.light) { --bg: #09090b; }
}

/* ✅ */
:root      { --bg: #ffffff; }
:root.dark { --bg: #09090b; }

Cookie storage does not support cross-tab theme sync. Use localStorage with initialTheme if you need it.

API

ThemeProvider

| Prop | Type | Default | Description | |------|------|---------|-------------| | themes | string[] | ["light", "dark"] | Available themes | | defaultTheme | string | "system" | Theme used when no preference is stored | | forcedTheme | string | - | Force a specific theme, ignoring user preference | | initialTheme | string | - | Server-provided theme that overrides storage on mount. User can still call setTheme to change it | | enableSystem | boolean | true | Detect system preference via prefers-color-scheme | | enableColorScheme | boolean | true | Set native color-scheme CSS property | | attribute | string \| string[] | "class" | HTML attribute(s) to set on target element ("class", "data-theme", etc.) | | value | Record<string, string> | - | Map theme names to attribute values | | target | string | "html" | Element to apply theme to ("html", "body", or a CSS selector) | | storageKey | string | "theme" | Key used for storage | | storage | "localStorage" \| "sessionStorage" \| "cookie" \| "none" | "localStorage" | Where to persist the theme. "cookie" reads/writes document.cookie and with @wrksz/themes/next also reads server-side for zero-flash SSR | | disableTransitionOnChange | boolean \| string | false | Suppress CSS transitions when switching themes. true disables all. Pass a CSS transition value (e.g. "background-color 0s, color 0s") to suppress only specific properties | | followSystem | boolean | false | Always follow system preference, ignores stored value on mount | | themeColor | string \| Record<string, string> | - | Update <meta name="theme-color"> on theme change | | nonce | string | - | CSP nonce for the inline script | | onThemeChange | (theme: string) => void | - | Called when theme changes. Receives the selected value (may be "system"). When system preference changes while theme is "system", fires with the resolved value |

useTheme

const {
  theme,         // Current theme - may be "system"
  resolvedTheme, // Actual theme - never "system"
  systemTheme,   // System preference: "light" | "dark" | undefined
  forcedTheme,   // Forced theme if set
  themes,        // Available themes
  setTheme,      // Set theme
} = useTheme();

Supports generics for full type safety:

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

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

getTheme

Reads the current theme from a cookie outside React. Available in @wrksz/themes/next.

// proxy.ts - sync, reads from Request
import { getTheme } from "@wrksz/themes/next";

export function proxy(request: Request) {
  const theme = getTheme(request, { defaultTheme: "dark" });
}

// layout.tsx - async, reads via cookies() from next/headers
const theme = await getTheme({ defaultTheme: "dark" });
return <html className={theme}>...</html>;

| Option | Type | Default | Description | |--------|------|---------|-------------| | storageKey | string | "theme" | Cookie name to read from | | defaultTheme | string | "system" | Returned when no valid theme is found | | themes | string[] | - | When provided, stored values not in the list fall back to defaultTheme |

useThemeValue

Returns the value from a map matching the current resolved theme. Returns undefined before the theme resolves on the client.

"use client";
import { useThemeValue } from "@wrksz/themes/client";

const label = useThemeValue({ light: "Switch to dark", dark: "Switch to light" });
const bg = useThemeValue({ light: "#ffffff", dark: "#0a0a0a" });
const icon = useThemeValue({ light: <SunIcon />, dark: <MoonIcon /> });

ThemedImage

Shows different images per theme. Renders a transparent placeholder on the server to avoid hydration mismatches.

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

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

Examples

Custom themes

<ThemeProvider themes={["light", "dark", "high-contrast"]}>
  {children}
</ThemeProvider>

Data attribute instead of class

<ThemeProvider attribute="data-theme">
  {children}
</ThemeProvider>
[data-theme="dark"] { --bg: #000; }
[data-theme="light"] { --bg: #fff; }

Multiple classes per theme

<ThemeProvider
  themes={["light", "dark", "dim"]}
  value={{ light: "light", dark: "dark high-contrast", dim: "dark dim" }}
>
  {children}
</ThemeProvider>

Switching away from "dark" correctly removes both dark and high-contrast.

Forced theme per page

// app/dashboard/layout.tsx
<ThemeProvider forcedTheme="dark">
  {children}
</ThemeProvider>

Scoped theming

Apply the theme to a specific element instead of <html>, so different sections can have independent themes simultaneously:

<ThemeProvider forcedTheme="dark" target="#landing-root" storage="none">
  <div id="landing-root">{children}</div>
</ThemeProvider>
#landing-root { --bg: #0a0a0a; --fg: #fafafa; }

Server-provided theme

Initialize from a server-side source (database, session) - overrides stored value on every mount:

export default async function RootLayout({ children }) {
  const userTheme = await getUserTheme();

  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider initialTheme={userTheme ?? undefined} onThemeChange={saveUserTheme}>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Nested provider in a Client Component

"use client";
import { ClientThemeProvider } from "@wrksz/themes/client";

export function AdminShell({ children }: { children: React.ReactNode }) {
  return (
    <ClientThemeProvider forcedTheme="dark">
      {children}
    </ClientThemeProvider>
  );
}

Suppress transitions on theme change

// Disable all transitions
<ThemeProvider disableTransitionOnChange>
  {children}
</ThemeProvider>

// Suppress only color properties, keep transform/opacity transitions intact
<ThemeProvider disableTransitionOnChange="background-color 0s, color 0s, border-color 0s">
  {children}
</ThemeProvider>

Import paths

| Import | Use for | |--------|---------| | @wrksz/themes/next | ThemeProvider, getTheme in Next.js (recommended) | | @wrksz/themes/client | useTheme, useThemeValue, ThemedImage, ClientThemeProvider | | @wrksz/themes | ThemeProvider for non-Next.js frameworks |

License

MIT