ThemeInitScript
ImplementedPre-hydration FOUC prevention script — server-rendered blocking inline <script> that reads a localStorage theme key (plus optional legacy keys, in read order) and stamps data-theme on <html> before first paint. Consolidates the storefront (DS-CORR-09-07), client-portal (DM-09) and docs-shell (DM-08) copies (W2).
patterns/molecules/themeinitscript.htmlPre-hydration FOUC prevention — a server-rendered blocking inline <script> that reads a localStorage theme key (plus optional legacy keys, in read order) and stamps data-theme on <html> before first paint. Consolidates the storefront (DS-CORR-09-07), client-portal (DM-09) and docs-shell (DM-08) copies (W2).
ThemeInitScript
Molecule — Theme & App Shell family
Pure Server Component — no hooks, no client directive. Renders exactly one <script> element whose body is derived only from props (every dynamic fragment goes through JSON.stringify), so server and client emit identical HTML. Read order: storageKey first, then each extraKeys entry; the first key holding "light" or "dark" wins, then prefers-color-scheme, then the fallback prop when storage is blocked.
Emitted script for storageKey="epasslive-theme" fallback="dark" extraKeys={["docs-theme"]} — rendered into <head> as the first child:
(() => {
try {
var keys = ["epasslive-theme","docs-theme"];
var stored = null;
for (var i = 0; i < keys.length; i++) {
var v = window.localStorage.getItem(keys[i]);
if (v === "light" || v === "dark") {
stored = v;
break;
}
}
var theme = stored !== null
? stored
: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
} catch (e) {
/* localStorage / matchMedia blocked — apply the surface default */
document.documentElement.setAttribute("data-theme", "dark");
}
})();Usage
Render exactly once per document, as the first child of <head> in a root layout — before any stylesheet that depends on data-theme. The script is blocking by design: no async, no defer, because it must run during HTML parsing, before first paint.
import { ThemeInitScript } from '@epasslive/design-system'
// app/layout.tsx (storefront) — key must equal the theme writer's key
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ThemeInitScript storageKey="epasslive-theme" fallback="dark" />
</head>
<body>{children}</body>
</html>
)
}Read order and fallback chain
The script resolves the theme through a fixed chain: storageKey first, then each extraKeys entry in order (first key holding "light" or "dark" wins — this preserves the docs two-key contract); if no key holds a valid value, prefers-color-scheme decides; and if storage AND matchMedia both throw (blocked storage), the fallback prop is applied. The whole body is wrapped in a try/catch and feature-checks window / localStorage / matchMedia, so it never throws on load.
SSR and hydration safety
ThemeInitScript is a pure Server Component: no hooks, no "use client". The script body is derived only from props and every dynamic fragment is escaped through JSON.stringify — no user input is ever interpolated — so the same props always emit the same HTML on server and client (no hydration mismatch). Pair it with suppressHydrationWarning on the <html> element, because the script mutates data-theme before React hydrates.
Props
storageKey*string—Primary localStorage key holding "light" | "dark" — must match the theme writer’s key exactly.
fallback*'light' | 'dark'—Theme applied when localStorage AND matchMedia both throw (blocked storage). Must match the surface’s SSR default.
extraKeysstring[][]Legacy keys checked in order when storageKey holds no valid value (docs reads epasslive-theme, then legacy docs-theme).
Props
ThemeInitScript
storageKeystring—Primary localStorage key holding "light" | "dark" — must match the theme writer's key exactly.
fallback'light' | 'dark'—Theme applied when localStorage AND matchMedia both throw (blocked storage). Must match the surface's SSR default.
extraKeysstring[][]Legacy keys checked in order when storageKey holds no valid value (docs reads epasslive-theme, then legacy docs-theme).
<ThemeInitScript />Accessibility
render
2 rulesUsage
Import
import { ThemeInitScript } from "@epasslive/design-system"Example 1
<ThemeInitScript storageKey={THEME_STORAGE_KEY} fallback="dark" />Example 2
<ThemeInitScript storageKey="epasslive-theme" fallback="light" extraKeys={["docs-theme"]} />Anti-patterns
— Do not add async/defer — the script must block before first paint
— Do not interpolate user input into the script body (props only, JSON.stringify-escaped)