@wrksz/themesv2.1.1
API Reference

ThemeProvider

API reference for the ThemeProvider component.

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

When rendered from a Server Component, the Next providers generate the bootstrap string on the server and pass it to the client boundary. The generator stays out of that client bundle; the script string is included in the HTML and the RSC payload. The typed Next factory remains a client-side path and still includes the generator. See createThemes.

Breakingv2.0.0

In 1.x, ThemeProvider from @wrksz/themes/next was an async Server Component that called cookies() for cookie/hybrid storage. From 2.0.0 it is synchronous and never reads cookies on the server. See Upgrading from 1.x.

Two variants are available depending on your framework:

@wrksz/themes/nextNext.js 16+ (no React 19 warning)

useServerInsertedHTML for <html>; synchronous adjacent script for later targets

@wrksz/themes/next/extendedSame-document sync or custom system theme names

Same Next bootstrap path, plus opt-in sync and system mapping

@wrksz/themesNested scopes and client-rendered React

Client-only provider

@wrksz/themes/client/extended-providerShadowRoot / client-owned themeRoot

Client-only extended provider

@wrksz/themes/scriptOther SSR frameworks

Server-rendered bootstrap script

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>
  );
}
Warning

Use ThemeProvider from @wrksz/themes/next directly in a server layout. The next entry also exports the server-only helper, so it is not a client-module entry. For nested providers inside Client Components, use ClientThemeProvider.

AvoidImporting the Next entry from a client module
app/providers.tsx
"use client";
import { ThemeProvider } from "@wrksz/themes/next";

export function Providers({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}
PreferRendering it in the root layout
app/layout.tsx
import { ThemeProvider } from "@wrksz/themes/next";

export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Props

readonly string[]Default["light", "dark"]

Available themes. Use as const to preserve custom theme unions in TypeScript

stringDefault"system"

Theme used when no preference is stored

"class"| "data-*"| ("class" | "data-*")[]Default"class"

HTML attribute(s) to set on the target element

Record<string, string>

Map theme names to attribute values

storageBreakingv2.0.0
"localStorage"| "sessionStorage"| "cookie"| "hybrid"| "none"Default"localStorage"

Where to persist the theme. The bootstrap reads cookies synchronously before paint. "hybrid" prefers the cookie and mirrors writes to localStorage for cross-tab sync.

Next provider no longer reads the cookie via cookies() on the server.
stringDefault"theme"

Key used for storage

booleanDefaulttrue

Detect system preference via prefers-color-scheme

forcedThemeBreakingv2.0.0
string

Force a specific theme, ignoring user preference.

Does not write to storage on init.
boolean | stringDefaultfalse

Suppress 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.

(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").

Show 9 more props
initialThemeBreakingv2.0.0
string

Server-provided theme that overrides storage on mount. User can still call setTheme to change it.

Mount init is sticky: later prop changes to storageKey / initialTheme do not re-run initialization (forcedTheme still updates via overlay).

Cookie attributes applied when writing the theme cookie. Used when storage is "cookie" or "hybrid".

stringDefault"html"

Element to apply theme to ("html", "body", or a CSS selector)

booleanDefaulttrue

Set native color-scheme CSS property

booleanDefaultfalse

Always follow system preference, ignores stored value on mount

string | Record<string, string>

Update <meta name="theme-color"> on theme change

string

CSP nonce for the inline script

ScriptHTMLAttributes<HTMLScriptElement>

Extra attributes for the bootstrap script, such as data-cfasync="false"

(error: unknown) => void

Reports unavailable, blocked, or quota-limited storage without interrupting theme updates

Extended provider

Heavier capabilities stay behind opt-in imports so the default provider bundle does not grow. @wrksz/themes/next/extended is synchronous and does not call cookies(), matching the default Next provider.

booleanDefaultfalse

Synchronize providers that share a storage key in the same document

{ light: string; dark: string }| Record<string, { light: string; dark: string }>

Serializable custom resolution for system-aware variants

Element | ShadowRoot

Client-only Shadow DOM or custom root. Not on @wrksz/themes/next/extended: a DOM object cannot go through the inline bootstrap. Import @wrksz/themes/client/extended-provider. Worked example: Shadow DOM. Use target when the root must be themed before hydration.

Security notes

ThemeProvider injects a small inline script to apply the initial theme before React hydrates. Configuration values are escaped for the <script> context, including themeColor, value, themes, forcedTheme, and initialTheme.

When your app uses a Content Security Policy, pass a request-scoped nonce and include the same value in script-src.

Cookie storage treats cookies as untrusted input. Stored values are validated against themes when provided, malformed cookie encoding falls back to defaultTheme, and cookie attributes are validated before writes.

Examples

Custom themes

const themes = ["light", "dark", "high-contrast"] as const;

<ThemeProvider themes={themes} defaultTheme="high-contrast">
  {children}
</ThemeProvider>

For full end-to-end inference across setTheme, , and theme-aware components, use createThemes with the same tuple.

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>
  );
}
Breakingv2.0.0

1.x: @wrksz/themes/next called cookies() and could set initialTheme for you. 2.0.0: zero-flash comes from the pre-paint bootstrap only; the provider stays static. Explicit SSR markup still needs getTheme.

When using @wrksz/themes/next, storage="cookie" keeps the provider in the static App Shell. Its bootstrap reads document.cookie synchronously and applies the class before the first paint:

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>
  );
}

Theme changes are written to the cookie automatically. If server-rendered markup must also depend on the theme, use getTheme explicitly.

Warning

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)

Breakingv2.0.0

In 1.x, hybrid storage also triggered a server cookies() read on the Next provider.

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

  • Read priority: cookie first, then localStorage
  • Writes: cookie and localStorage
  • App Shell: the bootstrap reads the cookie without making the layout request-time dynamic (1.x used server cookies() here)
  • 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>
  );
}

Next.js 16.3 Instant Navigations

Breakingv2.0.0

New in 2.0.0 as a consequence of removing implicit cookies(). In 1.x the Next provider was request-time for cookie/hybrid storage. See Upgrading from 1.x.

ThemeProvider does not call request-time APIs, so it can stay in a reusable App Shell when cacheComponents and partialPrefetching are enabled. Prefetches only render the static provider configuration; they never write theme cookies.

next/root-params is available by default in Next.js 16.3. Themes do not require a root-param integration. If your product intentionally stores a different theme per tenant or locale, scope the key in your app:

app/[tenant]/layout.tsx
import { tenant } from "next/root-params";
import { ThemeProvider } from "@wrksz/themes/next";

export default async function TenantLayout({ children }) {
  const currentTenant = await tenant();
  return (
    <ThemeProvider storageKey={`theme-${currentTenant}`}>
      {children}
    </ThemeProvider>
  );
}

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

Suppress CSS transitions only when the library actually changes the applied theme: during the inline script's initial run when SSR markup needs correction, and whenever the user switches themes. This prevents first-paint flash and animated jank without reinserting a transition override during no-op hydration:

<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, but only around a real DOM theme update.

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>

Use this with your consent manager rather than giving the theme library control over consent:

<ThemeProvider storage={hasPreferenceConsent ? "localStorage" : "none"}>
  {children}
</ThemeProvider>

Remove the previous key when consent is revoked if your policy requires deletion; storage="none" guarantees no further reads or writes.

Observe storage failures

<ThemeProvider onStorageError={(error) => reportError(error)}>
  {children}
</ThemeProvider>

The selected theme still updates in memory and on the DOM when persistence is unavailable.

Meta theme-color

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

Works with CSS variables too:

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

CookieOptions

Options applied when writing the theme cookie. Used when storage is "cookie" or "hybrid".

string

Cookie domain, e.g. ".example.com" to share across subdomains. Defaults to the current domain.

numberDefault31536000

Max age in seconds (1 year)

"Strict" | "Lax" | "None"Default"Lax"

SameSite attribute

boolean

Whether to add the Secure flag. Defaults to true on HTTPS.

stringDefault"/"

Cookie path