Skip to main content

Dynamic Theme Loading

Loading Themes at Runtime.

Dynamic Theme Loading

Overview​

In white-labeled applications, theme token files are often not available statically and must be fetched from a remote source (CDN, API, database). This guide demonstrates how to load theme tokens asynchronously at runtime while preserving server-side rendering (SSR) to avoid Flash of Unstyled Content (FOUC).

While the examples use Next.js, the core pattern applies to any SSR framework (Remix, SvelteKit, Nuxt, etc.).

The Challenge​

Traditional static imports assume theme tokens are available synchronously:

// ❌ Not possible with runtime-determined themes
import tokens from './brand-a-tokens.json';
const flattenedTokens = flattenTokens(tokens);
const theme = createTheme('brandA', { theme: flattenedTokens });

When the theme source is determined at runtime (e.g., from a database, user preference, or admin configuration), you need a different approach.

Solution Architecture​

The recommended pattern works across SSR frameworks by separating concerns:

  • Server (SSR): Fetch raw token JSON asynchronously during server render
  • Server → Client: Pass serializable token data via props or initial state
  • Client: Process tokens with flattenTokens()
  • Client: Generate theme with createTheme() and wrap app with ThemeProvider

This ensures:

  • ✅ Theme tokens are included in initial SSR payload (no FOUC)
  • ✅ Styles are server-rendered
  • ✅ Token processing happens once on the client
  • ✅ Theme source can be determined dynamically

Implementation​

Step 1: Server-Side Token Fetch​

Fetch raw token JSON during server-side rendering. This example uses Next.js App Router, but the concept applies to any SSR framework's loader/server function.

// Server Component - runs once per request
async function loadTokenData() {
try {
const tokenUrl = await getTokenUrlForCurrentDomain();
const response = await fetch(tokenUrl, {
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (!response.ok) {
throw new Error('Failed to fetch tokens');
}
const rawTokens = await response.json();
return {
rawTokens,
success: true,
};
} catch (error) {
console.error('Failed to load theme tokens:', error);
// Return fallback or empty state
return {
rawTokens: null,
success: false,
};
}
}
export default async function RootLayout({ children }) {
const tokenData = await loadTokenData();
return (
<html lang="en">
<body>
<ClientThemeWrapper tokenData={tokenData}>
{children}
</ClientThemeWrapper>
</body>
</html>
);
}

Step 2: Client Theme Wrapper​

Create a client component that processes tokens and provides the theme. This component receives the raw tokens from the server. Even though it's marked as a Client Component, it's pre-rendered during SSR, allowing Emotion to extract and inject styles into the initial HTML.

'use client';
import React, { useMemo } from 'react';
import { createTheme, flattenTokens } from '@uhg-abyss/web/tools/theme';
import { ThemeProvider } from '@uhg-abyss/web/ui/ThemeProvider';
interface ClientThemeWrapperProps {
children: React.ReactNode;
tokenData: {
rawTokens: any;
success: boolean;
};
}
export default function ClientThemeWrapper({
children,
tokenData,
}: ClientThemeWrapperProps) {
// Process tokens once when component mounts
const theme = useMemo(() => {
if (tokenData.success && tokenData.rawTokens) {
// Flatten and transform tokens
const flattenedTokens = flattenTokens(tokenData.rawTokens);
// Create theme with styles
return createTheme('yourBrand', { theme: flattenedTokens });
}
// Fallback to default theme
return createTheme('uhc');
}, [tokenData]);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}

Step 3: Use Themed Components​

All Abyss components will now use the dynamically loaded theme:

'use client';
import { Button } from '@uhg-abyss/web/ui/Button';
import { SearchInput } from '@uhg-abyss/web/ui/SearchInput';
export default function ThemeTestPage() {
return (
<main>
{/* Components automatically use loaded theme */}
<Heading>Dynamic Theme Applied</Heading>
<Button>Themed Button</Button>
</main>
);
}

Performance Considerations​

Caching Strategy​

Cache token fetches at multiple levels to reduce network overhead:

// Next.js example
const response = await fetch(tokenUrl, {
next: { revalidate: 3600 }, // Framework cache: 1 hour
headers: { 'Cache-Control': 'public, max-age=3600' }, // CDN cache
});
// Generic approach (works in any framework)
const response = await fetch(tokenUrl, {
headers: { 'Cache-Control': 'public, max-age=3600' },
});

Preloading​

Preload tokens in the document <head> to start fetching early:

<!-- Add to your HTML head (works in any framework) -->
<link
rel="preload"
href="/api/theme-tokens.json"
as="fetch"
crossorigin="anonymous"
/>
export default async function RootLayout({ children }) {
const tokenUrl = await getTokenUrl();
return (
<html>
<head>
<link
rel="preload"
href={tokenUrl}
as="fetch"
crossOrigin="anonymous"
/>
</head>
<body>{children}</body>
</html>
);
}

Troubleshooting​

FOUC (Flash of Unstyled Content)​

Symptom: page flashes unstyled before theme applies

Solution: fetch tokens during SSR, not in a client-side effect. Pass the raw tokens from server to client.

// ❌ Wrong - causes FOUC (client-side fetch)
function ClientWrapper({ children }) {
const [theme, setTheme] = useState(null);
useEffect(() => {
fetch('/api/tokens').then((data) => setTheme(createTheme(data)));
}, []);
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
// ✅ Correct - server-side fetch, then pass to client
// (Shown with Next.js, but use your framework's SSR data loading)
async function RootLayout({ children }) {
const tokenData = await loadTokenData(); // SSR: runs on server
return (
<ClientThemeWrapper tokenData={tokenData}>{children}</ClientThemeWrapper>
);
}

Serialization Errors​

Symptom: "Objects are not valid as a React child" or serialization errors

Cause: createTheme() generates Emotion CSS objects that contain functions and non-serializable JavaScript primitives. These cannot be sent from server to client components.

Solution: pass raw token JSON (serializable) from server to client. Do NOT call createTheme() on the server.

// ❌ Wrong - createTheme generates non-serializable Emotion objects
const theme = createTheme('brand', tokens); // Server-side
<ClientWrapper theme={theme} />; // Serialization error!
// ✅ Correct - pass raw JSON, process on client
const rawTokens = await fetch(url).then((r) => r.json());
<ClientWrapper tokenData={{ rawTokens }} />; // Client calls createTheme

Hydration Mismatches​

Symptom: React hydration warnings about mismatched content

Solution: ensure the same token data is used for both server render and client hydration. Don't conditionally process tokens based on window or client-only checks.

Table of Contents