Installation
Install Monsoon in React with Vite
In a Vite React app Monsoon is one import in src/main.jsx. Vite resolves monsooncss to the prebuilt stylesheet, bundles it with your own CSS and fingerprints the file. Components just use className.
Install
Start from npm create vite@latest my-app -- --template react (or react-ts), then choose one of the two setups.
Link the stylesheet in the root index.html. Vite leaves external URLs alone.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My app</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css" />
<script>(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')})()</script>
</head>
<body class="bg-background text-foreground antialiased">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>npm install monsooncssImport the package once, at the top of the entry file and before your own styles:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'monsooncss';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);'monsooncss' resolves to dist/monsoon.min.css through the package exports. Use 'monsooncss/css' for the readable build. Do not import 'monsooncss/dist/monsoon.min.css': that path is not exported, and Vite stops with a missing specifier error.
TypeScript 6 and newer check side effect imports by default (noUncheckedSideEffectImports), and a bare import 'monsooncss' has no type declaration, so tsc reports that it cannot find the module. Add a one line declaration file, for example monsooncss.d.ts containing declare module 'monsooncss';, in src/.
The Vite starter ships demo rules in src/index.css and src/App.css (a centered #root, its own colors). Empty or delete them, otherwise they paint over the Monsoon theme.
Use the classes
Classes go in className. Keep every class name as a complete string, even when it is conditional, so the purge step can find it later:
const tones = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
};
export function Button({ tone = 'primary', className = '', ...props }) {
return (
<button
type="button"
className={'rounded-lg px-4 py-2 font-medium ' + tones[tone] + ' ' + className}
{...props}
/>
);
}
export function PlanCard({ name, price, featured = false }) {
return (
<article className={featured ? 'rounded-2xl border bg-surface p-6 space-y-4 ring-2 ring-ring' : 'rounded-2xl border bg-surface p-6 space-y-4 shadow-sm'}>
<h2 className="text-lg font-semibold">{name}</h2>
<p className="text-3xl font-bold">{price}</p>
<Button tone={featured ? 'primary' : 'secondary'} className="w-full">
Choose {name}
</Button>
</article>
);
}import { PlanCard } from './components/PlanCard.jsx';
export default function App() {
return (
<main className="mx-auto max-w-5xl px-6 py-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<PlanCard name="Starter" price="$0" />
<PlanCard name="Team" price="$12" featured />
<PlanCard name="Business" price="$40" />
</div>
</main>
);
}Dark mode
Keep the head script from the install step in index.html in both setups: it runs before React loads, so the first paint already has the right scheme. The toggle then only flips the class and remembers the choice:
import { useState } from 'react';
function readTheme() {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}
export default function ThemeToggle() {
const [theme, setTheme] = useState(readTheme);
function toggle() {
const next = theme === 'dark' ? 'light' : 'dark';
const root = document.documentElement;
root.classList.remove('dark', 'light');
root.classList.add(next);
try { localStorage.setItem('monsoon-theme', next); } catch (e) {}
setTheme(next);
}
return (
<button
type="button"
onClick={toggle}
aria-pressed={theme === 'dark'}
className="rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
</button>
);
}To force a scheme on one part of the tree, add the class to that element: <aside className="dark bg-background text-foreground p-6">.
Theme with --seed
React passes custom properties in the style object as they are, so the seed can come from state, props or a user setting:
import { useState } from 'react';
export default function BrandPreview() {
const [seed, setSeed] = useState('#0d9488');
return (
<section style={{ '--seed': seed }} className="rounded-2xl bg-surface p-6 space-y-4">
<input type="color" value={seed} onChange={(e) => setSeed(e.target.value)} />
<button type="button" className="rounded-lg bg-primary text-primary-foreground px-4 py-2">
Primary
</button>
</section>
);
}In TypeScript, cast the object: style={{ '--seed': seed } as React.CSSProperties}. 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
Write the purged file before every production build:
{
"scripts": {
"dev": "vite",
"css:purge": "monsooncss purge \"index.html\" \"src/**/*.{js,jsx,ts,tsx}\" -o src/monsoon.purged.css",
"build": "npm run css:purge && vite build",
"preview": "vite preview"
}
}Then let Vite swap the import only for builds. import 'monsooncss' stays in src/main.jsx, development keeps the full file (so a new class works the moment you type it), and vite build bundles the purged one:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'node:url';
const purged = fileURLToPath(new URL('./src/monsoon.purged.css', import.meta.url));
export default defineConfig(({ command }) => ({
plugins: [react()],
resolve: {
alias: command === 'build' ? { monsooncss: purged } : {},
},
}));Using the CDN setup instead? Write to public/monsoon.css and change the <link> in index.html to /monsoon.css.
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.
- Server rendering? See Next.js or Remix.
Frequently asked questions
Does Monsoon need a PostCSS or Vite plugin in React?
No. It is a finished stylesheet, so Vite handles it like any other CSS import. Nothing scans your components in development.
Can I import Monsoon from a component instead of main.jsx?
You can, but import it once at the entry point. Vite then places it before component styles, and your own rules keep winning on equal specificity.