Passer au contenu principal

ThemeInitScript

Implemented

Pre-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).

Categorymolecules
Sourcepatterns/molecules/themeinitscript.html
AI HintRender exactly once per document, as the first child of <head> in a root layout. Pure Server Component — no hooks, no client directive. storageKey must equal the theme WRITER's persistence key.

Pre-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

Prop
Type
Default
Description
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.

extraKeys
string[]
[]

Legacy keys checked in order when storageKey holds no valid value (docs reads epasslive-theme, then legacy docs-theme).

Props

Preview

ThemeInitScript

Prop
Type
Required
Default
Description
storageKey
string
Required
—

Primary localStorage key holding "light" | "dark" — must match the theme writer's key exactly.

fallback
'light' | 'dark'
Required
—

Theme applied when localStorage AND matchMedia both throw (blocked storage). Must match the surface's SSR default.

extraKeys
string[]
Optional
[]

Legacy keys checked in order when storageKey holds no valid value (docs reads epasslive-theme, then legacy docs-theme).

Quick use:<ThemeInitScript />

Accessibility

ARIA rolescript

render

2 rules

FOUC prevention

Script executes synchronously during HTML parsing — no async/defer attributes

React 19 hydration

Server and client emit identical script HTML from the same props (deterministic, no hydration mismatch)

Usage

Import

tsxSnippet
import { ThemeInitScript } from "@epasslive/design-system"

Example 1

tsxSnippet
<ThemeInitScript storageKey={THEME_STORAGE_KEY} fallback="dark" />

Example 2

tsxSnippet
<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)

Design System v4.1.0 | ePassLive