@wrksz/themesv0.9.2
API Reference

ThemeProvider

API reference for the ThemeProvider component.

Edit on GitHub

Last updated on

ThemeProvider wraps your app, injects an anti-flash script before hydration, and manages theme state via ClientThemeProvider.

Two variants are available depending on your framework:

Since v0.6.0
ImportMechanismUse when
@wrksz/themes/nextuseServerInsertedHTMLNext.js 16+ (no React 19 warning)
@wrksz/themesinline <script> in RSCOther frameworks
app/layout.tsx (Next.js)
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>
  );
}

ThemeProvider from @wrksz/themes/next is an async Server Component. It must be used directly in a server component (e.g. layout.tsx) - it cannot be wrapped in a "use client" component. If you are migrating from next-themes and have a wrapper like this, remove it and use ThemeProvider directly in your layout.

// ❌ will throw: async Client Component error
"use client";
import { ThemeProvider } from "@wrksz/themes/next";
export function Providers({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}
// ✅ use directly in layout.tsx
import { ThemeProvider } from "@wrksz/themes/next";
export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

For nested providers inside Client Components, use ClientThemeProvider instead.

Props

PropTypeDefaultDescription
themesstring[]["light", "dark"]Available themes
defaultThemestring"system"Theme used when no preference is stored
forcedThemestring-Force a specific theme, ignoring user preference
initialThemestring-Server-provided theme that overrides storage on mount. User can still call setTheme to change it
enableSystembooleantrueDetect system preference via prefers-color-scheme
enableColorSchemebooleantrueSet native color-scheme CSS property
attribute"class" | "data-*" | ("class" | "data-*")[]"class"HTML attribute(s) to set on the target element
valueRecord<string, string>-Map theme names to attribute values
targetstring"html"Element to apply theme to ("html", "body", or a CSS selector)
storageKeystring"theme"Key used for storage
storage"localStorage" | "sessionStorage" | "cookie" | "hybrid" | "none""localStorage"Where to persist the theme. "hybrid" reads from cookie first (SSR-safe) and mirrors to localStorage for cross-tab sync. "cookie" reads/writes document.cookie and with @wrksz/themes/next also reads server-side. Since v0.9.0
cookieOptionsCookieOptions-Cookie attributes applied when writing the theme cookie. Only used when storage="cookie". See CookieOptions. Since v0.8.0
disableTransitionOnChangeboolean | stringfalseSuppress CSS transitions on initial load and when switching themes. true disables all transitions. Pass a CSS transition value (e.g. "background-color 0s, color 0s") to suppress only specific properties while keeping others (transforms, opacity, etc.) intact. Since v0.7.4
followSystembooleanfalseAlways follow system preference, ignores stored value on mount
themeColorstring | Record<string, string>-Update <meta name="theme-color"> on theme change
noncestring-CSP nonce for the inline script
onThemeChange(theme: string) => void-Called whenever the theme changes. Receives the selected value (may be "system"). When system preference changes while theme is "system", fires with the resolved value ("light" or "dark").

Examples

Custom themes

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

Data attribute instead of class

<ThemeProvider attribute="data-theme">
  {children}
</ThemeProvider>

Multiple classes per theme

Map a theme to multiple CSS classes using a space-separated value:

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

Force a theme per page

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

Server-provided theme

Use initialTheme to initialize from a server-side source (database, session, cookie) on every mount:

app/layout.tsx
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>
  );
}
Since v0.7.0

When using @wrksz/themes/next, storage="cookie" automatically reads the theme cookie server-side and renders the correct class on <html> from the first byte - no flash for any user, no boilerplate:

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

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="dark"
          storage="cookie"
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

The provider reads document.cookie on the client and cookies() from next/headers on the server. Theme changes are written to the cookie automatically.

Cookie storage does not support cross-tab theme sync. If you need cross-tab sync, use localStorage with initialTheme.

Hybrid storage (SSR + cross-tab sync)

Since v0.9.0

storage="hybrid" combines the best parts of cookie and local storage:

  • Read priority: cookie first, then localStorage
  • Writes: cookie and localStorage
  • SSR: cookie is available server-side with @wrksz/themes/next
  • Cross-tab: changes propagate through the storage event
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">
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Use cookieOptions.domain to share the theme preference between subdomains (e.g. app.example.com and api.example.com).

Works with both storage="cookie" and storage="hybrid":

app/layout.tsx
<ThemeProvider
  storage="hybrid"
  cookieOptions={{ domain: ".example.com" }}
>
  {children}
</ThemeProvider>

All cookieOptions fields have sensible defaults - you only need to specify what you want to override.

Suppress transitions on theme change

Since v0.7.4

Suppress CSS transitions on two occasions: when the inline script applies the theme on initial load, and whenever the user switches themes. This prevents both first-paint flash and animated jank:

<ThemeProvider disableTransitionOnChange>
  {children}
</ThemeProvider>

To keep some transitions intact (e.g. hover effects, animations) while only suppressing color-related ones:

<ThemeProvider disableTransitionOnChange="background-color 0s, color 0s, border-color 0s, fill 0s, stroke 0s">
  {children}
</ThemeProvider>

The string is injected as transition: <value> !important on all elements for two animation frames - both during the initial script run and on each subsequent theme change.

Always follow system preference

Use followSystem to ignore any stored value and always apply the system preference. Useful for apps where you want the theme to stay in sync with the OS without letting users override it:

<ThemeProvider followSystem>
  {children}
</ThemeProvider>

Unlike defaultTheme="system", which applies the system preference only on first visit and then stores the resolved value, followSystem re-reads prefers-color-scheme on every mount and ignores the stored value entirely.

Disable storage

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

Meta theme-color

<ThemeProvider themeColor={{ light: "#ffffff", dark: "#0a0a0a" }}>
  {children}
</ThemeProvider>

Works with CSS variables too:

<ThemeProvider themeColor="var(--color-background)">
  {children}
</ThemeProvider>

CookieOptions

Since v0.8.0

Options applied when writing the theme cookie. Only relevant when storage="cookie".

FieldTypeDefaultDescription
domainstringcurrent domainCookie domain, e.g. ".example.com" to share across subdomains
maxAgenumber31536000Max age in seconds (1 year)
sameSite"Strict" | "Lax" | "None""Lax"SameSite attribute
securebooleantrue on HTTPSWhether to add the Secure flag
pathstring"/"Cookie path

On this page