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
2How it works
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 →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
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}
294Every framework
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.
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.