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" | "none" | "localStorage" | Where to persist the theme. "cookie" reads/writes document.cookie and with @wrksz/themes/next also reads server-side - zero-flash SSR without boilerplate. Since v0.7.0 |
disableTransitionOnChange | boolean | string | false | Temporarily suppress CSS transitions 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.2 |
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
// 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>
);
}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.
Suppress transitions on theme change
Since v0.7.3Disable all transitions briefly when switching themes to prevent visual 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 during the switch.
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>