ThemeProvider
API reference for the ThemeProvider component.
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| Import | Mechanism | Use when |
|---|---|---|
@wrksz/themes/next | useServerInsertedHTML | Next.js 16+ (no React 19 warning) |
@wrksz/themes | inline <script> in RSC | Other frameworks |
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
| 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 | "class" | "data-*" | ("class" | "data-*")[] | "class" | HTML attribute(s) to set on the target element |
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" | "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 |
cookieOptions | CookieOptions | - | Cookie attributes applied when writing the theme cookie. Only used when storage="cookie". See CookieOptions. Since v0.8.0 |
disableTransitionOnChange | boolean | string | false | 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. Since v0.7.4 |
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 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
<ThemeProvider forcedTheme="dark">
{children}
</ThemeProvider>Server-provided theme
Use initialTheme to initialize from a server-side source (database, session, cookie) 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>
);
}Cookie storage (zero-flash SSR)
Since v0.7.0When 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:
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.0storage="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
storageevent
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>
);
}Sharing cookie across subdomains
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":
<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.4Suppress 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.0Options applied when writing the theme cookie. Only relevant when storage="cookie".
| Field | Type | Default | Description |
|---|---|---|---|
domain | string | current domain | Cookie domain, e.g. ".example.com" to share across subdomains |
maxAge | number | 31536000 | Max age in seconds (1 year) |
sameSite | "Strict" | "Lax" | "None" | "Lax" | SameSite attribute |
secure | boolean | true on HTTPS | Whether to add the Secure flag |
path | string | "/" | Cookie path |