alert-triangle
Troubleshooting
Common issues and fixes
Run into something unexpected? This page covers the most common issues developers encounter when integrating Theme Kit and how to resolve them quickly.
1Flash of Unstyled Content (FOUC)
A flash of unstyled or incorrectly-styled content before the theme loads.
The bootstrap script must run before the browser paints any content. If it is placed in the body or loaded asynchronously, the first paint uses the browser default and then snaps to the correct theme — causing a visible flash.
1import { createThemeBootstrapScript } from "@theme-kit/core";
2import { themes } from "./themes";
3
4export default function RootLayout({ children }) {
5 return (
6 <html lang="en" suppressHydrationWarning>
7 <head>
8 <script
9 dangerouslySetInnerHTML={{
10 __html: createThemeBootstrapScript({
11 themes,
12 defaultTheme: "mint-light",
13 }),
14 }}
15 />
16 </head>
17 <body>{children}</body>
18 </html>
19 );
20};2Theme Not Persisting
The selected theme resets after a page reload or closes and reopens.
Theme persistence is provided by a persistence adapter. The default adapter uses localStorage under the key
theme-selection — if another script writes to the same key, or if the user is in incognito mode with restricted storage, the theme will not persist. Create an explicit adapter with createThemePersistence() and a custom key, and verify that localStorage is accessible.1import { createThemeRuntime, createPersistencePlugin } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({
4 themes,
5 defaultTheme: "light",
6 plugins: [createPersistencePlugin({ key: "my-app-theme" })],
7});3CSS Variables Not Updating
CSS custom properties on <html> or <body> are not reflecting the active theme.
CSS variable injection is handled by the
cssVariables binding. If it is set to false (or omitted), the runtime will not write --theme-* variables to the DOM. Ensure the provider is mounted and the binding is configured (it accepts a prefix and a target element).1import { ThemeProvider } from "@theme-kit/react";
2
3export default function RootLayout({ children }) {
4 return (
5 <ThemeProvider
6 themes={themes}
7 defaultTheme="light"
8 cssVariables={{ prefix: "theme-" }}
9 dom={{}}
10 >
11 {children}
12 </ThemeProvider>
13 );
14}4Hydration Mismatch
Next.js or React reports a hydration mismatch warning in the console.
During server rendering the theme is resolved from cookies, headers, or the default. On the client the bootstrap script resolves it independently. If these two resolutions disagree, React detects a mismatch. Ensure
defaultTheme matches the server-side resolution and add suppressHydrationWarning to <html>.1// 1. Resolve the theme server-side the same way the runtime does
2// 2. Pass it as defaultTheme to the provider
3// 3. suppressHydrationWarning on <html> handles the attribute diff
4import { createThemeBootstrapScript } from "@theme-kit/core";
5import { ThemeProvider } from "@theme-kit/react";
6import { themes } from "./themes";
7
8export default function RootLayout({ children }) {
9 return (
10 <html lang="en" suppressHydrationWarning>
11 <head>
12 <script
13 dangerouslySetInnerHTML={{
14 __html: createThemeBootstrapScript({
15 themes,
16 defaultTheme: resolvedTheme,
17 }),
18 }}
19 />
20 </head>
21 <body>
22 <ThemeProvider themes={themes} defaultTheme={resolvedTheme}>
23 {children}
24 </ThemeProvider>
25 </body>
26 </html>
27 );
28}5Theme Flicker on Navigation
The theme briefly flashes to the default on client-side route changes.
In SPA frameworks the provider must wrap the router. If the provider is inside a route or a layout that unmounts during navigation, the runtime is destroyed and re-created — causing a brief flicker as it re-reads from storage. Move the provider above the router so the runtime persists across all route changes.
1// Ensure the ThemeProvider is ABOVE the router so the runtime
2// survives client-side navigation without re-mounting.
3import { createThemeBootstrapScript } from "@theme-kit/core";
4import { ThemeProvider } from "@theme-kit/react";
5import { themes } from "./themes";
6
7export default function RootLayout({ children }) {
8 return (
9 <html lang="en" suppressHydrationWarning>
10 <head>
11 <script
12 dangerouslySetInnerHTML={{
13 __html: createThemeBootstrapScript({
14 themes,
15 defaultTheme: "light",
16 }),
17 }}
18 />
19 </head>
20 <body>
21 <ThemeProvider themes={themes} defaultTheme="light">
22 {/* AppRouter / layout from next/navigation lives here */}
23 {children}
24 </ThemeProvider>
25 </body>
26 </html>
27 );
28}6Scoped Theme Not Working
A ThemeScope has no visible effect on the targeted subtree.
ThemeScope must directly wrap the subtree it should affect. If the scope is a sibling or ancestor at the wrong level, the scoped tokens will not reach the target components. Pass the scope as a theme name, a family, or a { family, mode } object — the themes prop is for local definitions, not theme names. Verify the wrapping hierarchy.1import { ThemeScope } from "@theme-kit/react";
2
3export default function DashboardPage() {
4 return (
5 <div>
6 <h1>Dashboard</h1>
7 <ThemeScope theme="corporate-light">
8 {/* Only this subtree uses the scoped theme */}
9 <WidgetPanel />
10 <AnalyticsCard />
11 </ThemeScope>
12 </div>
13 );
14}7Transitions Not Animating
Theme switches are instant instead of animated.
Transitions are opt-in. If
transition.enabled is false (the default), every theme switch is instant. Set enabled: true and choose a preset to enable animated transitions.1const runtime = createThemeRuntime({
2 themes,
3 defaultTheme: "light",
4 transition: {
5 enabled: true,
6 preset: "smooth",
7 duration: 360,
8 easing: "cubic-bezier(0.4, 0, 0.2, 1)",
9 },
10});8Build Error: Module Not Found
Your bundler throws a Module Not Found error for @theme-kit packages.
This typically happens when
@theme-kit/core and the framework integration (e.g. @theme-kit/react) are on different versions. The packages share internal modules and must be the exact same version. Reinstall both packages at the latest version.1# Ensure @theme-kit/core and the framework integration share
2# the exact same version — mismatched versions cause module
3# resolution failures.
4
5npm install @theme-kit/core@latest @theme-kit/react@latest
6
7# Or with pnpm
8pnpm add @theme-kit/core@latest @theme-kit/react@latest9Accessibility Audit Failing
Automated accessibility audits flag color contrast failures in your theme.
Theme tokens must meet WCAG contrast ratios for the text sizes where they are used. Use
validateThemeContrast() from @theme-kit/core to audit every token pair at build time and catch failures before they reach production.1import { validateThemeContrast } from "@theme-kit/core";
2
3// validateThemeContrast checks one theme against its registry
4const result = validateThemeContrast(theme, { themes });
5
6if (!result.valid) {
7 console.error("Failing token pairs (AA large text):");
8 result.checks
9 .filter((check) => !check.passesAALarge)
10 .forEach((check) => {
11 console.error(
12 ` ${check.foregroundToken} on ${check.backgroundToken}:`,
13 );
14 console.error(` ratio ${check.ratio.toFixed(2)}:1`);
15 });
16 process.exit(1);
17}10Multi-Window Out of Sync
Changing the theme in one tab does not update other open tabs.
Theme Kit uses
BroadcastChannel to sync theme changes across tabs, and falls back to a SharedWorker and then to window storage events when it is unavailable (e.g. incognito, third-party iframes). Pass an explicit broadcast adapter from createMultiWindowSync() to control the strategy.1import { createThemeRuntime, createMultiWindowSync } from "@theme-kit/core";
2
3const runtime = createThemeRuntime({
4 themes,
5 defaultTheme: "light",
6 // BroadcastChannel is used by default. If it is blocked or unavailable
7 // the sync falls back to a SharedWorker, then to window "storage" events
8 // (works in all browsers that support localStorage). Prefer "sharedworker"
9 // to start from the worker strategy:
10 broadcast: createMultiWindowSync({ prefer: "auto" }),
11});