refract v0.1 · beta
Guides / Next.js / SSR

Next.js / SSR

Because theme.css is just a string, server-rendering is straightforward: pick the theme per request, render its <style> into the initial HTML, and the page arrives already themed — no flash. This uses the App Router; nothing here is refract-specific beyond theme.css.

1 · Compile once, pick per request

Compile at module scope so themes are built once per server process, not per request — then select by tenant. override() children are cheap, so a brand map costs almost nothing.

lib/theme.ts
import { createTheme } from "@theme-registry/refract";
import { createCssAdapter } from "@theme-registry/refract-css";

const base = createTheme(raw, { adapter: createCssAdapter() });

const brands = {
  acme:   base.override({ colors: { brand: { base: "#e8590c", text: "#ffffff" } } }),
  globex: base.override({ colors: { brand: { base: "#2f9e44", text: "#ffffff" } } }),
};

export const getTheme = (brand?: string) => brands[brand] ?? base;

2 · Inject in the root layout (no FOUC)

The root layout is a Server Component — derive the tenant from the request, render the theme's <style> in <head>. It's server-rendered into the first byte, so there's no unthemed flash.

app/layout.tsx
import { headers, cookies } from "next/headers";
import { getTheme } from "@/lib/theme";

export default async function RootLayout({ children }) {
  const host = (await headers()).get("host") ?? "";
  const theme = getTheme(host.split(".")[0]);        // tenant from subdomain
  const mode = (await cookies()).get("theme")?.value;   // "dark" | "light" | undefined

  return (
    <html lang="en" data-theme={mode}>
      <head>
        <style dangerouslySetInnerHTML={{ __html: theme.css }} />
      </head>
      <body>{children}</body>
    </html>
  );
}
Reading headers() / cookies() makes the route dynamic (rendered per request) — expected for per-tenant theming. A single static theme needs neither; see the build-time option below.

3 · Dark mode without a flash

Author modes and the OS preference is handled automatically by the emitted @media block. For a manual choice, set data-theme on <html> server-side from a cookie (as above) — the attribute is in the first byte, so the manual theme applies before paint, with no client flip flash.

Using the classes

In any component — server or client — read classes exactly as in the React guide: import the theme and call theme.getClass("components", "buttons", "primary"). Server Components can import the theme module directly.

Prefer static? If the theme isn't per-request, skip runtime injection entirely: emit theme.css to a file at build time and import "./theme.css" in the layout — refract leaves the client bundle completely. (Pages Router: the same idea, injecting in _document.)
Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.