Installation
Install Monsoon in Next.js
In the App Router, global CSS belongs in the root layout. Import Monsoon in app/layout.tsx, put the theme script in <head> and you can use className everywhere, in server and client components alike.
Install
These examples assume a project created with npx create-next-app@latest and the app/ directory. If you chose src/, the files live in src/app/.
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'My app',
};
const themeScript = "(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')})()";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css" />
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="bg-background text-foreground antialiased">{children}</body>
</html>
);
}npm install monsooncssimport type { Metadata } from 'next';
import 'monsooncss';
import './globals.css';
export const metadata: Metadata = {
title: 'My app',
};
const themeScript = "(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')})()";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body className="bg-background text-foreground antialiased">{children}</body>
</html>
);
}Next.js allows global CSS imports from node_modules in the root layout, and monsooncss resolves to the minified stylesheet through the package exports.
declare module 'monsooncss';That one line file at the project root keeps next build from failing its type check: TypeScript 6 and newer reject a side effect import that has no type declaration.
suppressHydrationWarning on <html> is required. The head script adds dark or light to that element before React hydrates, so the class differs from the server HTML on purpose. The attribute only silences that one element, not its children.
If create-next-app filled app/globals.css with another utility setup, clear it so two resets do not compete.
Use the classes
Server components can use every class, since nothing runs in the browser to apply them. Conditional classes stay as full strings:
import Link from 'next/link';
type Plan = { name: string; price: string; href: string; featured?: boolean };
export function PlanCard({ name, price, href, featured = false }: Plan) {
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>
<Link
href={href}
className={featured ? 'block rounded-lg bg-primary text-primary-foreground px-4 py-2 text-center font-medium hover:bg-primary/90' : 'block rounded-lg bg-secondary text-secondary-foreground px-4 py-2 text-center font-medium hover:bg-secondary/80'}
>
Choose {name}
</Link>
</article>
);
}import { PlanCard } from '@/components/PlanCard';
export default function PricingPage() {
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" href="/signup" />
<PlanCard name="Team" price="$12" href="/signup?plan=team" featured />
<PlanCard name="Business" price="$40" href="/contact" />
</div>
</main>
);
}Dark mode
The head script in the layout picks the scheme before the first paint. The toggle is a small client component that does not read the theme while rendering, so the server and client markup always match:
'use client';
export function ThemeToggle() {
function toggle() {
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) {}
}
return (
<button type="button" onClick={toggle} className="rounded-lg border px-3 py-2 text-sm hover:bg-muted">
Toggle theme
</button>
);
}If you want the label to show the current scheme, read the class in a useEffect after mount, never during render, or the first client render will not match the server HTML.
A section can opt out of the page scheme with the class alone: <aside className="dark bg-background text-foreground p-6"> renders dark on every page.
Theme with --seed
Because the seed is a plain custom property, a server component can set it from data, and the themed HTML arrives ready to paint:
import { getTeam } from '@/lib/teams';
export default async function TeamLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ team: string }>;
}) {
const { team } = await params;
const { brandColor } = await getTeam(team);
return (
<div style={{ '--seed': brandColor } as React.CSSProperties} className="min-h-dvh bg-background">
{children}
</div>
);
}Validate stored colors on the server (a hex pattern such as ^#[0-9a-fA-F]{6}$ is enough). 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
Generate the purged file before next build:
{
"scripts": {
"dev": "next dev",
"css:purge": "monsooncss purge \"app/**/*.{js,jsx,ts,tsx,mdx}\" \"components/**/*.{js,jsx,ts,tsx}\" -o app/monsoon.purged.css",
"build": "npm run css:purge && next build",
"start": "next start"
}
}Then alias the import for production only. The layout keeps import 'monsooncss'; next dev loads the full file and next build the purged one:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
turbopack: {
resolveAlias:
process.env.NODE_ENV === 'production'
? { monsooncss: './app/monsoon.purged.css' }
: {},
},
};
export default nextConfig;If you still build with webpack (next build --webpack), add the same alias to config.resolve.alias in the webpack function instead. With the CDN setup, write to public/monsoon.css and link /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.
- Using the Pages Router? Import
monsooncssinpages/_app.tsxand put the head script inpages/_document.tsx.
Frequently asked questions
Why does Next.js warn about a hydration mismatch on the html element?
The head script changes the class on <html> before React hydrates. Add suppressHydrationWarning to that element, as in the layout above.
Does Monsoon work with React Server Components?
Yes. It is plain CSS with no runtime, so server components, client components and static export all render the same classes.
Should I use next/script for the theme script?
A plain inline <script> in the root layout <head> is enough and runs before paint. next/script with beforeInteractive also works, but it adds nothing here.