Scoped theming
Apply independent themes to different sections of your app simultaneously.
By default ThemeProvider applies the theme to <html>. Use the target prop to scope the theme to a specific element instead, so different sections of your app can have independent themes at the same time.
Use ClientThemeProvider for nested providers - the inline anti-flash script is only needed in the root layout. ClientThemeProvider handles state without rendering an extra script.
For a server-rendered root scope that targets "body" or a descendant selector, use ThemeProvider from @wrksz/themes/next. Since v2.0.0 It emits the parser-blocking bootstrap immediately after the provider subtree so the target already exists when the script runs, while the default "html" target continues to use useServerInsertedHTML.
Try it
Two client providers, each targeting its own element with storage="none". Switching one never affects the other or the page.
Grace Hopper
Landing
Each section keeps its own theme.
#scoped-landing …
Alan Turing
Dashboard
Switching one never touches the other.
#scoped-dashboard …
"use client";
import { ClientThemeProvider } from "@wrksz/themes/client";
export function Sections() {
return (
<>
<ClientThemeProvider target="#landing" attribute="data-theme" storage="none">
<section id="landing">{/* … */}</section>
</ClientThemeProvider>
<ClientThemeProvider target="#dashboard" attribute="data-theme" storage="none" defaultTheme="dark">
<section id="dashboard">{/* … */}</section>
</ClientThemeProvider>
</>
);
}Setup
import { ClientThemeProvider } from "@wrksz/themes/client";
export default function LandingLayout({ children }) {
return (
<ClientThemeProvider forcedTheme="dark" target="#landing-root" storage="none">
<div id="landing-root">{children}</div>
</ClientThemeProvider>
);
}import { ClientThemeProvider } from "@wrksz/themes/client";
export default function DashboardLayout({ children }) {
return (
<ClientThemeProvider forcedTheme="light" target="#dashboard-root" storage="none">
<div id="dashboard-root">{children}</div>
</ClientThemeProvider>
);
}#landing-root { --bg: #0a0a0a; --fg: #fafafa; }
#dashboard-root { --bg: #ffffff; --fg: #0a0a0a; }Use storage="none" when the theme is forced - there's nothing to persist.
User-selectable scoped theme
If the user can change the theme within a section, omit forcedTheme and use a separate storageKey per section so the themes are stored independently:
import { ClientThemeProvider } from "@wrksz/themes/client";
export default function DashboardLayout({ children }) {
return (
<ClientThemeProvider target="#dashboard-root" storageKey="dashboard-theme">
<div id="dashboard-root">{children}</div>
</ClientThemeProvider>
);
}Target selector
The target prop accepts any CSS selector, plus the shortcuts "html" (default) and "body":
<ClientThemeProvider target="body"> // applies to <body>
<ClientThemeProvider target="#my-element"> // applies to #my-element
<ClientThemeProvider target=".my-class"> // applies to .my-class (first match)target is a CSS selector in the light DOM. The Next bootstrap can apply it before paint. For a client-owned Element or ShadowRoot with no selector and no bootstrap, use themeRoot on @wrksz/themes/client/extended-provider instead. See Shadow DOM.