Installation
Install Monsoon in Django templates
Django templates render plain HTML, so Monsoon needs no Python package and no build step. Link it in base.html, either from the CDN or as a static file served by {% static %}.
Install
{% load static %}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}My site{% endblock %}</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>
{% block head %}{% endblock %}
</head>
<body class="bg-background text-foreground antialiased">
{% block content %}{% endblock %}
<script src="{% static 'js/theme-toggle.js' %}" defer></script>
</body>
</html>Copy the stylesheet into your static folder once (repeat after upgrading):
npm install monsooncss
cp node_modules/monsooncss/dist/monsoon.min.css static/css/monsoon.min.cssNo Node.js on the machine? Download the same file instead: curl -o static/css/monsoon.min.css https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css.
STATICFILES_DIRS = [BASE_DIR / "static"]{% load static %}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}My site{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/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>
{% block head %}{% endblock %}
</head>
<body class="bg-background text-foreground antialiased">
{% block content %}{% endblock %}
<script src="{% static 'js/theme-toggle.js' %}" defer></script>
</body>
</html>The stylesheet only references data: URLs, so ManifestStaticFilesStorage and WhiteNoise process it without missing file errors during collectstatic.
Use the classes
Template tags work inside class like anywhere else. Keep each branch a full class list:
<article class="rounded-2xl border bg-surface p-6 space-y-4 {% if plan.featured %}ring-2 ring-ring{% else %}shadow-sm{% endif %}">
<h2 class="text-lg font-semibold">{{ plan.name }}</h2>
<p class="text-3xl font-bold">{{ plan.price }}</p>
<a href="{% url 'checkout' plan.slug %}" class="block rounded-lg px-4 py-2 text-center font-medium {% if plan.featured %}bg-primary text-primary-foreground hover:bg-primary/90{% else %}bg-secondary text-secondary-foreground hover:bg-secondary/80{% endif %}">
Choose {{ plan.name }}
</a>
</article>{% extends "base.html" %}
{% block content %}
<main class="mx-auto max-w-5xl px-6 py-12">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{% for plan in plans %}
{% include "components/plan_card.html" with plan=plan %}
{% endfor %}
</div>
</main>
{% endblock %}Form fields get their classes from widget attributes, so rendered forms match the rest of the page:
from django import forms
FIELD = "w-full rounded-lg border bg-background px-3 py-2 text-sm"
class ContactForm(forms.Form):
email = forms.EmailField(widget=forms.EmailInput(attrs={"class": FIELD}))
message = forms.CharField(widget=forms.Textarea(attrs={"class": FIELD, "rows": 4}))Dark mode
The head script in base.html sets the scheme before paint. Add a button to any template and the toggle script to static/js:
<button type="button" id="theme-toggle" class="rounded-lg border px-3 py-2 text-sm hover:bg-muted">
Toggle theme
</button>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);
button.setAttribute('aria-pressed', String(next === 'dark'));
try { localStorage.setItem('monsoon-theme', next); } catch (e) {}
});A section can be pinned to a scheme with the class alone: <aside class="dark bg-background text-foreground p-6">.
Theme with --seed
Store the brand color on a model and validate its format, then print it inline. Autoescaping protects HTML, not CSS, so the validator is what keeps the value safe:
from django.core.validators import RegexValidator
from django.db import models
hex_color = RegexValidator(r"^#[0-9a-fA-F]{6}$", "Use a hex color such as #0d9488.")
class Team(models.Model):
name = models.CharField(max_length=100)
brand_color = models.CharField(max_length=7, default="#0d9488", validators=[hex_color])<section style="--seed: {{ team.brand_color }}" class="rounded-2xl bg-surface p-6 space-y-4">
<h2 class="text-xl font-bold text-primary">{{ team.name }}</h2>
<a href="{% url 'team_detail' team.pk %}" class="inline-block rounded-lg bg-primary text-primary-foreground px-4 py-2">Open</a>
</section>Model validators run in full_clean() and in model forms, not on a bare save(). 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
Scan the project templates, the app templates, any Python that holds class strings and your scripts:
{
"private": true,
"scripts": {
"css:purge": "monsooncss purge \"templates/**/*.html\" \"*/templates/**/*.html\" \"*/forms.py\" \"static/js/**/*.js\" -o static/css/monsoon.css"
},
"devDependencies": {
"monsooncss": "^0.2.0"
}
}npm run css:purge
python manage.py collectstatic --noinputChange the <link> to {% static 'css/monsoon.css' %}, and run the script in your deploy step before collectstatic. Without a package.json, npx monsooncss purge ... does the same.
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
Frequently asked questions
Do I need a Django package for Monsoon?
No. Monsoon is a stylesheet. Link it from the CDN or serve it with the static files app you already have.
Does Monsoon work with Jinja2 templates in Django?
Yes. Replace {% static %} with your environment helper, for example static(), and the rest of the markup is the same.