@42-central/vite-plugin-theme
v0.25.0
Published
Vite plugin that injects @42/ui-react's anti-FOUC theme init script into index.html
Readme
A Vite plugin that stops
@42-central/ui-react
apps from flashing the wrong theme on load. It puts the kit's theme-init script
at the top of <head> in index.html, in both vite dev and vite build.
Why
useTheme() stores the visitor's "light" | "dark" | "system" choice and
keeps data-theme on <html> in sync. React only runs after first paint,
though. So without help, a dark-mode visitor sees one light frame before the
hook catches up.
In Next.js, <ThemeScript /> covers that gap from the server render. A Vite
SPA has no server render, so the script has to sit in index.html and block
parsing. It also can't be type="module", because Vite defers module
scripts, and the flash comes back. This plugin injects a classic inline
<script> through Vite's transformIndexHtml hook, so you never edit the
HTML by hand.
Install
Install it as a dev dependency, next to the kit:
npm install -D @42-central/vite-plugin-theme
pnpm add -D @42-central/vite-plugin-theme
yarn add --dev @42-central/vite-plugin-theme
bun add --dev @42-central/vite-plugin-themevite 7 or 8 is a peer dependency. The plugin builds its script with
@42-central/ui-react, which it lists as its own dependency. Keep both
packages on the same version: they are released together.
Usage
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { themeInitPlugin } from "@42-central/vite-plugin-theme";
export default defineConfig({
plugins: [react(), themeInitPlugin()],
});Then read and change the theme from any component with the kit's hook:
import { useTheme } from "@42-central/ui-react";
export function ThemeToggle() {
const { resolvedTheme, setMode } = useTheme();
return (
<button onClick={() => setMode(resolvedTheme === "dark" ? "light" : "dark")}>
Toggle theme
</button>
);
}Options
themeInitPlugin(options?) takes the same options as the kit's
buildThemeInitScript(), plus nonce:
| Option | Type | Default | What it does |
| -------------------- | ---------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------- |
| storageKey | string | "ui-theme" | localStorage key that holds the stored mode. |
| defaultColorScheme | "light" \| "dark" \| "system" | "system" | Mode used when nothing is stored yet. |
| forceColorScheme | "light" \| "dark" | – | Pins the theme and ignores both storage and the OS preference. |
| attribute | "data-theme" \| "class", or an array of them | "data-theme" | What to write on <html>: the data-theme attribute, a .dark class, or both. |
| nonce | string | – | CSP nonce added to the injected <script>. |
Keep it in sync with useTheme()
There is no provider holding this configuration, so the script and the hook
can't share it. Pass storageKey, defaultColorScheme, forceColorScheme
and attribute with the same values to the plugin and to every
useTheme() call. If they differ, the script and the hook disagree on first
load.
// vite.config.ts
themeInitPlugin({ forceColorScheme: "dark", attribute: ["class", "data-theme"] });// anywhere in the app
useTheme({ forceColorScheme: "dark", attribute: ["class", "data-theme"] });A shared const THEME_OPTIONS = { … } imported by both files is the easiest
way to keep them aligned.
Content Security Policy
If your CSP blocks inline scripts, pass the nonce your server puts in the
script-src directive:
themeInitPlugin({ nonce: "{{CSP_NONCE}}" });The value goes into the built index.html verbatim. Use whatever placeholder
your server or edge function replaces per request.
What it injects
One tag, first in <head>:
<head>
<script>/* ~600 bytes: read localStorage, resolve "system", set data-theme */</script>
<!-- the rest of your head -->
</head>The script runs once and registers no listeners. Following the OS preference
live after load is useTheme()'s job.
Not using Vite?
The ui-react README
covers the alternatives: a prebuilt theme-init.js you copy into public/,
or pasting the output of buildThemeInitScript() by hand. Next.js apps use
<ThemeScript /> instead.
