Django Seedkit

Blog · Frontend

Tailwind CSS in Django without Node

A Tailwind build needs no package.json, no node_modules, and no webpack. Tailwind ships a standalone binary. django-tailwind-cli downloads that binary and runs it from a management command.

uv add django-tailwind-cli
# config/settings/base.py
INSTALLED_APPS = [
    # ...
    "django_tailwind_cli",
]

STATICFILES_DIRS = [BASE_DIR / "assets"]
TAILWIND_CLI_VERSION = "4.3.2"   # check the latest tag on
                                 # github.com/tailwindlabs/tailwindcss/releases

Pin the CLI version in every environment, not only in production. A Tailwind 4 point release can change the build output, and an unpinned CLI makes CI build different CSS.

Django stops at boot if the assets/ directory does not exist. Django checks every STATICFILES_DIRS entry at startup, and raises an error if the path is not a directory. The message names STATICFILES_DIRS and not Tailwind, so a fresh clone looks broken for another reason. Run mkdir -p assets, and add one tracked file, because Git does not store an empty directory.

The template tag

One tag covers both environments. In development it points at the file that the watcher rebuilds. In production it points at the collected file. No template needs an {% if debug %} branch.

{% load tailwind_cli %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {% tailwind_css %}
</head>
<body class="bg-gray-50 text-gray-900">
    {% block content %}{% endblock %}
</body>
</html>

Keep the {% load tailwind_cli %} line at the top of the template. Without it Django raises TemplateSyntaxError and reports tailwind_css as an invalid block tag. A copied snippet drops that line often.

Development

python manage.py tailwind runserver

That command runs the watcher and the development server under one autoreloader, and it accepts every runserver flag. The first run downloads the binary to .django_tailwind_cli/ and creates a source CSS file. The package adds that directory to .gitignore.

Tailwind 4 finds templates through @source directives in the source CSS. It does not scan the project. Add a new templates directory, then add an @source line for it. Without that line, the build drops every class in those templates.

Or give the task to an agent. It reads the same setup from a maintained reference file and applies it to the repository:

/django-seedkit add Tailwind

The reference holds the full setup, the Docker build step, DaisyUI, and custom error pages: tailwind.md · What Django Seedkit is