Installation
Install Monsoon in SvelteKit
SvelteKit has a natural home for each piece: the stylesheet import in src/routes/+layout.svelte, and the theme script in src/app.html, which is served before any JavaScript runs.
Install
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-background text-foreground antialiased">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>npm install monsooncss<script>
import 'monsooncss';
let { children } = $props();
</script>
{@render children()}Keep the theme script in src/app.html exactly as in the CDN tab, minus the <link>.
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/ (next to app.d.ts). svelte-check reports the same error.
Use the classes
<script>
let { plan } = $props();
</script>
<article
class={plan.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 class="text-lg font-semibold">{plan.name}</h2>
<p class="text-3xl font-bold">{plan.price}</p>
<a
href="/signup?plan={plan.slug}"
class="block rounded-lg px-4 py-2 text-center font-medium"
class:bg-primary={plan.featured}
class:text-primary-foreground={plan.featured}
class:bg-muted={!plan.featured}
>
Choose {plan.name}
</a>
</article><script>
import PlanCard from '$lib/components/PlanCard.svelte';
let { data } = $props();
</script>
<main class="mx-auto max-w-5xl px-6 py-12">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{#each data.plans as plan (plan.slug)}
<PlanCard {plan} />
{/each}
</div>
</main>Dark mode
Components also render on the server, where document does not exist. Read the class in onMount and keep the server label neutral:
<script>
import { onMount } from 'svelte';
let isDark = $state(false);
onMount(() => {
isDark = document.documentElement.classList.contains('dark');
});
function toggle() {
isDark = !isDark;
const theme = isDark ? 'dark' : 'light';
const root = document.documentElement;
root.classList.remove('dark', 'light');
root.classList.add(theme);
try { localStorage.setItem('monsoon-theme', theme); } catch (e) {}
}
</script>
<button
type="button"
onclick={toggle}
aria-pressed={isDark}
class="rounded-lg border px-3 py-2 text-sm hover:bg-muted"
>
{isDark ? 'Light mode' : 'Dark mode'}
</button>The head script lives in src/app.html rather than in <svelte:head>, because app.html is plain HTML that runs before hydration. If you enable kit.csp, add the hash of that inline script to script-src.
Theme with --seed
Load the brand color on the server and apply it with the style: directive, so the first response is already themed:
export async function load({ locals }) {
return { brandColor: locals.team?.brandColor ?? '#0d9488' };
}<script>
import 'monsooncss';
let { data, children } = $props();
</script>
<div class="min-h-dvh bg-background" style:--seed={data.brandColor}>
{@render children()}
</div>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
{
"scripts": {
"dev": "vite dev",
"css:purge": "monsooncss purge \"src/**/*.{svelte,html,js,ts}\" -o src/lib/monsoon.purged.css",
"build": "npm run css:purge && vite build",
"preview": "vite preview"
}
}import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { fileURLToPath } from 'node:url';
const purged = fileURLToPath(new URL('./src/lib/monsoon.purged.css', import.meta.url));
export default defineConfig(({ command }) => ({
plugins: [sveltekit()],
resolve: {
alias: command === 'build' ? { monsooncss: purged } : {},
},
}));import 'monsooncss' in the layout now loads the full file under vite dev and the purged file in builds. With the CDN setup, write to static/monsoon.css and change the <link> in src/app.html to %sveltekit.assets%/monsoon.css.
Classes used only in class: directives, such as class:bg-muted={...} above, are not detected by the purge CLI. Put them in a quoted string elsewhere or add them with --safelist bg-muted.
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 not import Monsoon in src/app.html?
app.html is a template, not a module, so it cannot import from node_modules. Use the root layout for the import and app.html for the CDN link and the head script.
Does it work with prerendering and adapter-static?
Yes. The stylesheet and the head script end up in every prerendered page.