Vite Plugin
Optional: zero-flash before the bundle loads
The themeKitVitePlugin injects a synchronous bootstrap script into your index.html at build time so the persisted theme is applied on the very first frame — before the JavaScript bundle even loads.
1Why a Vite Plugin?
Since @theme-kit/react (and Vue, Svelte, Solid) 1.2.0, the providers are flash-proof out of the box — they inject the blocking bootstrap themselves before first paint. The vite plugin is only needed if you want the theme applied on the absolute first frame before the bundle loads (relevant for slow bundles or network-sensitive previews).
- Pre-bundle first frame — the bootstrap script runs synchronously from
index.htmlbefore the bundle loads, so even the raw HTML frame is themed. - No runtime overhead — the script is injected at build time and does not ship as a separate chunk. It runs once and is never re-evaluated.
- SSR-compatible — works alongside server-rendered frameworks (Next.js, Nuxt, Remix) by mirroring the same bootstrap in the server-rendered HTML.
- Tree-shakeable — only the theme definitions you pass in are included in the bootstrap output. No unused code reaches the client.
1// Without the plugin:
2// 1. Browser loads HTML → default theme rendered
3// 2. JS hydrates → reads localStorage → swaps theme
4// 3. User sees a flash of the wrong theme
5//
6// With the plugin:
7// 1. Build injects the bootstrap script at the top of <head>
8// 2. Browser loads HTML → script runs synchronously before render
9// 3. data-theme is set on <html> before the first paint
10// 4. CSS picks up the correct custom properties immediately
11// 5. No flash, ever2Setup
1import { defineConfig } from "vite";
2import react from "@vitejs/plugin-react";
3import { themeKitVitePlugin } from "@theme-kit/core/vite";
4import { customThemes } from "./src/themes";
5
6export default defineConfig({
7 plugins: [
8 react(),
9 themeKitVitePlugin({
10 themes: customThemes,
11 defaultTheme: "light",
12 }),
13 ],
14});@theme-kit/core/vite, not the main entry point. This keeps the core bundle clean when you only need the runtime.3What It Does
- Reads the theme definitions and
localStoragekey at build time. - Generates a self-contained IIFE that checks
localStorage, falls back to the default theme, and setsdata-themeon<html>. - Injects the script as
<head>-prepend withenforce: "pre"so it runs before any other head scripts.
1<!doctype html>
2<html lang="en">
3 <head>
4 <!-- Injected by the plugin at build time -->
5 <script>
6 /* Theme Kit bootstrap — runs before first paint */
7 (function() {
8 var key = "theme-selection";
9 var stored = localStorage.getItem(key);
10 var theme = stored || "light";
11 document.documentElement.setAttribute("data-theme", theme);
12 })();
13 </script>
14 <title>My App</title>
15 </head>
16 <body>…</body>
17</html>4Options
1import { themeKitVitePlugin } from "@theme-kit/core/vite";
2
3themeKitVitePlugin({
4 // All theme definitions registered with the runtime
5 themes: customThemes,
6
7 // Theme applied on first visit (before localStorage)
8 defaultTheme: "light",
9
10 // OS-level mode: "light" | "dark" | "system"
11 initialMode: "system",
12
13 // Scoped family to activate initially
14 initialFamily: "brand",
15
16 // localStorage key for the persisted selection
17 // Default: "theme-selection"
18 storageKey: "theme-selection",
19
20 // CSS custom property prefix
21 // Default: "theme-"
22 prefix: "theme-",
23});themesRequired. The theme definitions to include in the bootstrap.defaultThemeTheme applied on first visit before localStorage has a value.initialModeOS-level color scheme hint. Defaults to "system".initialFamilyScoped family to activate on first visit.storageKeylocalStorage key for the persisted selection. Defaults to "theme-selection".prefixCSS custom property prefix. Defaults to "theme-".5SSR Integration
The Vite plugin only transforms index.html, which is not used by SSR frameworks. Instead, embed the same bootstrap script directly in your server-rendered markup so the data-theme attribute is present on the first server-painted HTML.
1// The Vite plugin is framework-agnostic, but SSR frameworks
2// need the bootstrap on the server-rendered HTML too.
3// For Next.js, inject the script in your root layout:
4
5// app/layout.tsx
6const ThemeScript = () => (
7 <script
8 dangerouslySetInnerHTML={{
9 __html: `
10 (function() {
11 var key = "theme-selection";
12 var stored = localStorage.getItem(key);
13 var theme = stored || "light";
14 document.documentElement.setAttribute("data-theme", theme);
15 })();
16 `,
17 }}
18 />
19);
20
21export default function RootLayout({ children }) {
22 return (
23 <html lang="en" suppressHydrationWarning>
24 <head>
25 <ThemeScript />
26 </head>
27 <body>{children}</body>
28 </html>
29 );
30}1export default defineNuxtConfig({
2 app: {
3 head: {
4 script: [
5 {
6 innerHTML: `
7 (function() {
8 var key = "theme-selection";
9 var stored = localStorage.getItem(key);
10 var theme = stored || "light";
11 document.documentElement.setAttribute("data-theme", theme);
12 })();
13 `,
14 },
15 ],
16 },
17 },
18});suppressHydrationWarning to the <html> element. The server cannot know what the client has in localStorage, so the attribute will differ on first render.