Skip to content

Zero Flash

@theme-kit/core — bootstrap & SSR-safe hydration

Theme Kit never lets the wrong theme — or no theme at all — reach the screen. The server resolves the persisted selection, inlines the CSS variables, and a blocking bootstrap script covers the rest, so the very first paint is already the user's theme.

1The problem

Most theming libraries hydrate the theme from client-side JS — which leaves a flash of the wrong theme on reload and SSR.
Without Theme Kit
1Server returns the default (light) page
2Blocking layout paint of the light theme
3Client JS reads the saved dark theme
4Re-render — and a visible flash of the wrong theme
With Theme Kit
1Server resolves the persisted selection (cookies/fingerprint)
2HTML renders with data-theme + inline CSS variables
3First paint is already the user's theme
4Hydration takes over — no re-render, no flash, no mismatch
No flash of incorrect theme
The dark-mode user never sees a white interstitial — the first paint is already dark.
No hydration mismatch
React and other frameworks hydrate against the exact markup the server emitted. No attribute storms, no re-render of the wrong theme.
Works across reloads
Cookies/localStorage + fingerprinting mean a reload renders exactly what the user last chose — even after a deploy changes the theme set.

2How it works

One owned pipeline — rules that run server-side before anything is painted, and a blocking script for everything else.
1
Persist
Every change is mirrored to persistence — localStorage in a SPA, cookies for SSR — so the server knows the selection on the next request.
2
Resolve
Fingerprint-validated cookies/selection are resolved to a concrete theme: family + effective mode (system resolves against prefers-color-scheme).
3
Generate
The resolved theme is flattened to semantic CSS variables (--theme-color-*, --theme-radius-*, fonts, shadows…).
4
Render
The server returns <html> with data-theme, data-theme-mode, data-theme-family, the .dark class and color-scheme — with the variables inlined.
5
Block
A blocking bootstrap script in <head> applies the persisted selection before the first paint for client-only/static setups.
6
Paint
The very first paint is already the user's theme. There is nothing to flash.
7
Hydrate
React/the framework hydrates against the exact same markup (no mismatch) and the runtime takes over all future changes.
What about the native scrollbar?

A dark-mode user opening a page while the default theme is light sees the native light scrollbar flash before hydration — even when colors are already correct. Theme Kit's scrollbar story uses the same trick: the bootstrap hides the native scrollbar before first paint, then the theme-aware overlay engine takes over.

Custom Scrollbar →
Transitions wait for the initial state

The transition engine only starts after the initial theme is established — the first paint is never animated. A user who arrives with the dark theme selected gets dark immediately, then any later switch (families, modes, scopes) animates through the configured plan.

Animation & Transition →

3The core primitives

Framework-agnostic — use them directly in any head, or let a framework package do it for you.
ts
1import {
2  createThemeBootstrapScript,
3  buildThemeCssMap,
4  darkModeCSSTemplate,
5} from "@theme-kit/core";
6
7// 1) A blocking, dependency-free <script> for <head>. It reads the
8//    persisted selection (localStorage "theme-selection"), resolves the
9//    family + effective mode ("system" → prefers-color-scheme), and
10//    writes the CSS variables + data-theme attrs before first paint.
11const bootstrapScript = createThemeBootstrapScript({
12  themes,
13  defaultTheme: "light",
14  initialMode: "system",
15  storageKey: "theme-selection",
16});
17
18// 2) For SSR: build the CSS map once, inline the resolved theme's
19//    variables into the returned <html>, and add the blocking script.
20const cssMap = buildThemeCssMap(themes, { prefix: "theme-" });
21
22// 3) When the initial mode is "system", also emit the dark-mode media
23//    fallback so dark-mode users get correct colors with zero JS.
24const darkVars = cssMap[resolvedFamily + ":dark"] ?? null;
25if (darkVars) {
26  const darkFallback = darkModeCSSTemplate(darkVars);
27  // → emit as a <style> wrapped in @media (prefers-color-scheme: dark)
28}
29

4Every framework

Same runtime, different integration — the visual result is identical, only the wiring changes.
Flash-proof by default (1.2.0)

The React, Vue, Svelte and Solid providers now inject the blocking bootstrap themselves — you just wrap your app in the provider and the persisted theme is applied before first paint. The core createThemeBootstrapScript and the Vite plugin remain available for custom heads and the pre-bundle first frame.

React (SPA)
tsx
1// Flash-proof out of the box. The provider reads the persisted selection,
2// applies it before first paint, and injects a blocking bootstrap <script>
3// into <head> — no vite plugin or manual index.html script required.
4import { ThemeProvider } from "@theme-kit/react";
5
6root.render(
7  <ThemeProvider themes={themes} defaultTheme="light" initialMode="system">
8    <App />
9  </ThemeProvider>,
10);

SSR frameworks with dedicated server integration (Next, Nuxt, Astro, Angular, Remix) resolve the theme on the server. Client providers (React, Vue, Svelte, Solid) read the persisted selection and apply it before first paint automatically. Either way the bootstrap runs before the first paint.

5What's next

Zero flash is the start — theme switching should feel smooth too.
Zero Flash — Theme Kit