Scoped Theme
@theme-kit/core · ThemeScope
Most theming is global. Scoped theming is not: it lets you apply a whole theme — palette, radius, typography — to a subtree, turning it into a sandboxed island that ignores the global theme. Great for embedded widgets, preview panes, marketing sections, and multi-brand surfaces inside one app.
Pick your framework
1ThemeScope — pick your framework
The same scoping primitive in every adapter. Select a framework to see the exact implementation.
1import { ThemeScope } from "@theme-kit/react";
2import type { ThemeTransitionOptions } from "@theme-kit/core";
3
4const scopeTransition: ThemeTransitionOptions = { duration: 300, easing: "cubic-bezier(0.4, 0, 0.2, 1)" };
5
6<ThemeProvider themes={themes}>
7 <ThemeScope theme="plum-dark" transition={scopeTransition}>
8 <Sidebar />
9 </ThemeScope>
10</ThemeProvider>How a scope selects its theme |Pass an exact theme name like
plum-dark, a family name — the scope resolves the family's theme for the current mode — or split them into family + mode props. Omit both to mirror the global selection inside a fresh boundary. Every prop is reactive: when themechanges, the scope re-resolves and animates without a remount. An unregistered name falls back to the first theme.2Imperative scoping
When you don't control JSX or markup — e.g. a video player or legacy DOM — scope a raw element directly.
1import {
2 createThemeRuntime,
3 createScopedThemeBinding,
4} from "@theme-kit/core";
5
6const runtime = createThemeRuntime({ themes, defaultTheme: "light" });
7
8const binding = createScopedThemeBinding(themes, el, "plum-dark");
9
10binding.destroy();1"use client";
2
3import { useRef } from "react";
4import { useScopedTheme } from "@theme-kit/react";
5
6export function Preview({ themeName }: { themeName: string | null }) {
7 const ref = useRef<HTMLDivElement | null>(null);
8 useScopedTheme(ref, themeName);
9
10 return <div ref={ref}>This div follows {themeName ?? "the global theme"}.</div>;
11}3Local themes
Themes defined on the scope itself — for genuinely isolated components that ship their own palette. No second runtime is created.
1import { ThemeScope } from "@theme-kit/react";
2import type { ThemeDefinition } from "@theme-kit/core";
3import { defineTheme } from "@theme-kit/core";
4
5const compactTheme: ThemeDefinition = defineTheme({
6 name: "compact-light",
7 meta: { family: "compact", mode: "light" },
8 tokens: {
9 colors: {
10 background: "#ffffff",
11 foreground: "#18181b",
12 primary: "#6366f1",
13 primaryForeground: "#ffffff",
14 card: "#fafafa",
15 },
16 },
17});
18
19<ThemeScope themes={[compactTheme]} theme="compact-light">
20 <EditorToolbar />
21</ThemeScope>local themes resolve first |A scope's
themes are layered on top of the provider's registry: a local theme with the same name shadows the parent's, and anything a local theme doesn't define falls through to the app's themes (its extends chain is merged, so inherited tokens resolve). Late-loaded packs work too — swapping the themes array re-resolves the scope in place. The imperative equivalent passes localThemes to createScopedThemeBinding.4Transitions
Scoped theme changes animate through the same transition engine — inheriting the provider's config unless you say otherwise.
1import { ThemeScope } from "@theme-kit/react";
2
3<ThemeScope theme="plum-dark" />
4
5<ThemeScope theme="plum-dark" transition={false} />
6
7<ThemeScope
8 theme="plum-dark"
9 transition={{ duration: 150 }}
10/>inheritance model |
Provider transition → Scope → inherited defaults → local overrides. Passing an object merges over the provider's ThemeTransitionOptions (duration, easing, preset, View Transitions), so a scoped change animates smoothly with the rest of the app while letting you tweak just what's different. Reduced-motion users get an instant swap.5How a scope works
A scope is a plain element that emits the theme's variables into its own subtree — nothing leaks outside.
1<ThemeProvider themes={themes}>
2 <ThemeScope theme="mint-light">
3 <Dashboard />
4
5 <ThemeScope theme="plum-dark">
6 <CodeEditor />
7 </ThemeScope>
8 </ThemeScope>
9</ThemeProvider>Scope tree
Global ThemeProvider
mint-light Dashboard
plum-dark CodeEditor
- The scope element renders every token of the target theme as
--theme-*CSS custom properties on itself, so children just consume them. - Tailwind is supported directly — scopes also emit
--color-*and--radius-*aliases, so utilities likebg-cardpick up the scoped palette. - Dark scoped themes add a
darkclass to the scope element, keeping Theme Kit's "@custom-variant dark" Tailwind trick working inside the island. - Scopes nest arbitrarily — an inner
ThemeScopeoverrides the outer one for its own subtree. - The scope resolves its theme from the registry, so it updates automatically when the global mode changes (family fallback), and re-renders when the underlying theme definition is swapped.
- Server-rendered scopes pre-paint correctly too: an explicit selection (exact name, or a light/dark family mode) renders its variables inline, while an OS-dependent selection (a
systemmode, or a boundary scope following a system selection) ships a@media (prefers-color-scheme: dark)block so the first paint already matches the OS — no flash of the wrong scoped theme before hydration.
6What's next
Scoped theming is one advanced feature — continue through the rest.