Installation
Install Monsoon in Electron
An Electron window is a web page, so Monsoon works in the renderer like anywhere else. The differences are a content security policy, offline use and a native title bar that should follow your theme.
Install
A desktop app should not need a network connection to look right, so the local file is the better default. The CDN works too if your app is always online.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:">
<title>My app</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css">
<script src="./theme-init.js"></script>
</head>
<body class="bg-background text-foreground antialiased">
<main class="p-6 space-y-4">
<h1 class="text-2xl font-bold">Hello from Electron</h1>
</main>
<script src="./renderer.js"></script>
</body>
</html>npm install monsooncss<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:">
<title>My app</title>
<link rel="stylesheet" href="../node_modules/monsooncss/dist/monsoon.min.css">
<script src="./theme-init.js"></script>
</head>
<body class="bg-background text-foreground antialiased">
<main class="p-6 space-y-4">
<h1 class="text-2xl font-bold">Hello from Electron</h1>
</main>
<script src="./renderer.js"></script>
</body>
</html>Install it as a regular dependency, not a dev dependency, so packagers include it in the app archive. If your renderer goes through a bundler (the Electron Forge Vite template or electron-vite), use import 'monsooncss'; in the renderer entry instead of the <link>.
(function(){var t;try{t=localStorage.getItem('monsoon-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light')})();A strict policy (script-src 'self') blocks inline scripts, so the theme script lives in its own file, loaded without defer or type="module" so it still runs before the first paint. img-src data: lets the inline SVG images some styles use load.
Use the classes
Classes work in static HTML and in markup you create from the renderer script:
<div class="flex min-h-dvh">
<aside class="w-64 shrink-0 border-r bg-surface p-4 space-y-1">
<a href="#inbox" class="block rounded-lg bg-primary text-primary-foreground px-3 py-2 text-sm font-medium">Inbox</a>
<a href="#archive" class="block rounded-lg px-3 py-2 text-sm text-muted-foreground hover:bg-muted">Archive</a>
</aside>
<main class="flex-1 min-w-0 p-6 space-y-4">
<article class="rounded-2xl border bg-surface p-6 shadow-sm space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Team plan</h2>
<span class="rounded-full bg-secondary text-secondary-foreground px-2 py-1 text-xs font-medium">Popular</span>
</div>
<p class="text-muted-foreground">Shared projects, roles and a single invoice for everyone.</p>
<button type="button" class="w-full rounded-lg bg-primary text-primary-foreground px-4 py-2 font-medium hover:bg-primary/90">
Choose plan
</button>
</article>
</main>
</div>Dark mode
Toggle the class in the renderer as usual. To keep the native title bar and menus in step, also tell the main process, which sets nativeTheme.themeSource:
const button = document.getElementById('theme-toggle');
button.addEventListener('click', () => {
const root = document.documentElement;
const next = root.classList.contains('dark') ? 'light' : 'dark';
root.classList.remove('dark', 'light');
root.classList.add(next);
try { localStorage.setItem('monsoon-theme', next); } catch (e) {}
window.appTheme?.set(next);
});const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('appTheme', {
set: (theme) => ipcRenderer.invoke('theme:set', theme),
});const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron');
const path = require('node:path');
ipcMain.handle('theme:set', (_event, theme) => {
if (['light', 'dark', 'system'].includes(theme)) nativeTheme.themeSource = theme;
});
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 1100,
height: 720,
webPreferences: { preload: path.join(__dirname, 'preload.js') },
});
win.loadFile(path.join(__dirname, 'index.html'));
});Setting nativeTheme.themeSource also changes prefers-color-scheme inside the window, so with system Monsoon simply follows the OS again.
Theme with --seed
Under style-src 'self' an inline style="--seed: ..." attribute is blocked, but setting the property from script is not. Use style.setProperty:
function applySeed(color) {
document.documentElement.style.setProperty('--seed', color);
try { localStorage.setItem('monsoon-seed', color); } catch (e) {}
}
try {
const saved = localStorage.getItem('monsoon-seed');
if (saved) applySeed(saved);
} catch (e) {}For a fixed brand color, put :root { --seed: #0d9488; } in your own stylesheet instead. Every semantic token (primary, secondary, accent, surfaces, muted text, borders and the focus ring) is derived from --seed in the browser. Theming lists the tokens.
Using the purge CLI
File size matters less when the stylesheet is read from disk, but a purged file still parses faster and shrinks the app archive:
{
"scripts": {
"start": "electron .",
"css:purge": "monsooncss purge \"src/**/*.{html,js}\" -o src/monsoon.css"
}
}Change the <link> to ./monsoon.css and run the script before you package. Monsoon can then move to devDependencies, since the app no longer reads it from node_modules.
The purge CLI reads your files as plain text and keeps every class name it can see. It cannot see a name that is assembled at runtime, so write each class in full: isActive ? 'bg-primary' : 'bg-muted' survives a purge, 'bg-' + color does not. Names that only exist at runtime (from a database, a CMS or an API) go in --safelist bg-success,bg-danger.
The purged file always keeps the reset, the theme tokens, the .dark and .light scopes and the keyframes, so dark mode and --seed keep working. See the purge CLI reference for every option.
Next steps
- Browse every utility and its variants in the class reference.
- Read how the scheme is chosen in Dark mode, and how tokens are derived in Theming.
Frequently asked questions
Why does the stylesheet not load in my packaged Electron app?
Usually because monsooncss is a dev dependency and the packager skipped it, or because the policy blocks it. Make it a regular dependency (or ship the purged copy) and check style-src.
Does dark mode follow the OS in Electron?
Yes. Without a stored choice Monsoon follows prefers-color-scheme, which Electron maps to the OS setting or to nativeTheme.themeSource.