Installation
Install Monsoon in Astro
Astro renders HTML on the server and ships no JavaScript by default, which suits a stylesheet with no runtime. Import Monsoon in your base layout and every page that uses the layout gets it.
Install
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css" />
<script is:inline>(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">
<slot />
</body>
</html>npm install monsooncss---
import 'monsooncss';
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<script is:inline>(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">
<slot />
</body>
</html>Astro bundles the imported stylesheet and adds the <link> to the head for you.
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/. astro check reports the same error.
Keep is:inline on the theme script. Without it Astro processes the script as a module, which runs after the page has painted.
Use the classes
Use class for static names and class:list to combine them with conditions. Both render to a plain class attribute:
---
interface Props {
name: string;
price: string;
featured?: boolean;
}
const { name, price, featured = false } = Astro.props;
---
<article class:list={['rounded-2xl border bg-surface p-6 space-y-4', { 'ring-2 ring-ring': featured, 'shadow-sm': !featured }]}>
<h2 class="text-lg font-semibold">{name}</h2>
<p class="text-3xl font-bold">{price}</p>
<a
href="/signup"
class:list={[
'block rounded-lg px-4 py-2 text-center font-medium',
featured ? 'bg-primary text-primary-foreground hover:bg-primary/90' : 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
]}
>
Choose {name}
</a>
</article>---
import Layout from '../layouts/Layout.astro';
import PlanCard from '../components/PlanCard.astro';
---
<Layout title="Pricing">
<main class="mx-auto max-w-5xl px-6 py-12">
<div class="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>
</Layout>Dark mode
A processed <script> is fine for the toggle itself, since the head script has already set the scheme:
<button id="theme-toggle" type="button" class="rounded-lg border px-3 py-2 text-sm hover:bg-muted">
Toggle theme
</button>
<script>
document.getElementById('theme-toggle')?.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) {}
});
</script>With view transitions (<ClientRouter />), Astro replaces the attributes of <html> on every navigation, which drops the class. Turn the head script into a named function and run it again after each swap:
<script is:inline>
function applyTheme() {
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');
}
applyTheme();
document.addEventListener('astro:after-swap', applyTheme);
</script>With view transitions, register the toggle listener inside document.addEventListener('astro:page-load', ...) as well, so it binds to the new button after each navigation.
Theme with --seed
Astro accepts a style object and keeps custom property names as they are:
---
const { brandColor = '#0d9488' } = Astro.props;
---
<section style={{ '--seed': brandColor }} class="rounded-2xl bg-surface p-6 space-y-4">
<slot />
<button type="button" class="rounded-lg bg-primary text-primary-foreground px-4 py-2">Primary</button>
</section>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": "astro dev",
"css:purge": "monsooncss purge \"src/**/*.{astro,html,md,mdx,js,ts,jsx,tsx,vue,svelte}\" -o src/styles/monsoon.purged.css",
"build": "npm run css:purge && astro build"
}
}A tiny inline integration points monsooncss at the purged file for astro build only:
import { defineConfig } from 'astro/config';
import { fileURLToPath } from 'node:url';
const purged = fileURLToPath(new URL('./src/styles/monsoon.purged.css', import.meta.url));
export default defineConfig({
integrations: [
{
name: 'monsoon-purged-css',
hooks: {
'astro:config:setup': ({ command, updateConfig }) => {
if (command === 'build') {
updateConfig({ vite: { resolve: { alias: { monsooncss: purged } } } });
}
},
},
},
],
});With the CDN setup, write to public/monsoon.css and change the <link> href 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. Content collections count too: include src/content/** in the glob if your Markdown uses classes.
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
Do framework islands in Astro see Monsoon classes?
Yes. The stylesheet is global, so React, Vue, Svelte and Solid islands use the same classes as .astro components.
Should the stylesheet go in every page?
Import it once in the shared layout. Every page that uses the layout gets it, and Astro deduplicates the file.