The short version
- You can use Tailwind v4 with Django without an API or a Django package: the Tailwind CLI compiles a CSS file into your app's static folder, and your templates load it with the static tag.
- Link the compiled output.css in your templates, not the globals.css input file, which only holds Tailwind's @import line.
- Tailwind v4 needs no config file. It scans your project for class names, Django templates included, and only generates CSS for the classes it finds.
- While you work, run the Tailwind watcher and Django's runserver in two terminals. The watcher rebuilds the CSS every time you save a template.
- Before you deploy, build the CSS with --minify first and run collectstatic after it, so the fresh output.css is what gets collected.
To use Tailwind CSS v4 with Django, run the Tailwind CLI in watch mode so it compiles a CSS file into your app's static folder, and load that compiled file in your templates with the static tag. You don't need a REST API or a Django package for it.
Below I set up a fresh project step by step and cover what to run before you deploy. I tested every file on this page with Django 6.1.2 and Tailwind 4.3.3. The video above covers the same setup on Django 6.0.
You don't need an API to use npm tools with Django
I see a lot of people struggle to get Django, or any Python framework, working with Node.js tools. Many of them assume the only way is to build a backend API with Django REST Framework and put a React app in front of it. For Tailwind that's massive overkill.
Tailwind is a build tool. It reads your files for class names and writes a CSS file with the styles for them. Django already knows how to serve static files and render templates. The two meet at a single file on disk: Tailwind writes output.css, and your template links to it. Neither one has to know the other exists, and you keep Django templates and everything else you like about Django.
The same idea works for any npm tool whose output is a file. Run it from an npm script that writes into a static folder, and load the result from your templates.
How Tailwind v4 finds your classes
Tailwind v4 dropped the tailwind.config.js file. You configure it from CSS instead, and the whole setup is one line in a CSS file:
@import "tailwindcss";
When the CLI runs, it scans the files in the folder you run it from for anything that looks like a class name, and generates CSS only for the classes it finds. It skips node_modules and anything in your .gitignore. Django templates are plain text files, so it picks up the classes in them without any setup. My demo's minified CSS came out at under 5 KB, because the template only uses four classes.
This setup vs django-tailwind vs django-tailwind-cli
If you search for this, you'll find two Django packages that wrap Tailwind for you. Here's how they compare with doing it by hand:
| Tailwind CLI by hand (this page) | django-tailwind | django-tailwind-cli | |
|---|---|---|---|
| Needs Node.js | Yes | Only in npm mode | No |
| How Tailwind gets installed |
npm install or bun add
|
manage.py tailwind install |
Downloads the standalone CLI on first run |
| Extra Django app | No | Yes, a theme app | No |
| How you run it | An npm script plus runserver
|
manage.py tailwind dev or tailwind start
|
manage.py tailwind runserver |
| Tailwind plugins | Any npm plugin | All in npm mode, only DaisyUI in standalone mode | DaisyUI is built in |
Both packages work. The by-hand version is what I show in the video: two npm packages and one script. It has nothing to learn beyond Tailwind's own docs, and if you add another npm tool to the project later, it runs the same way.
Set up the project
I use uv for every Python project. It manages the virtual environment and the dependencies, and it's much faster than pip install. For the npm side I use Bun, because it installs packages faster than npm. Plain npm works the same way if you prefer it.
Django 6 needs Python 3.12 or newer. Create the Django project, then set up uv and npm inside it:
uvx --from django django-admin startproject tailwind_integration
cd tailwind_integration
uv init
uv add django
npm init -y
bun add tailwindcss @tailwindcss/cli
uv init also creates a main.py that Django doesn't need, so delete it. The @tailwindcss/cli package is what gives you the tailwindcss command. In v4 it's a separate package from tailwindcss itself.
Now create an app with a templates folder and a static folder:
uv run python manage.py startapp app
mkdir -p app/templates app/static
Here's the finished project, without the generated files we don't touch:
tailwind_integration/
├── app/
│ ├── static/
│ │ ├── globals.css
│ │ └── output.css
│ └── templates/
│ └── base.html
├── tailwind_integration/
│ ├── settings.py
│ └── urls.py
├── manage.py
├── package.json
└── pyproject.toml
output.css doesn't exist yet. Tailwind creates it the first time it runs.
Register the app and add a URL
Add the app to INSTALLED_APPS in tailwind_integration/settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app',
]
Django only looks in an app's templates and static folders once the app is installed, so if you skip this, it can't find the template or the CSS.
Then replace tailwind_integration/urls.py with this:
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path('admin/', admin.site.urls),
path('', TemplateView.as_view(template_name='base.html')),
]
TemplateView renders a template without a view function, which is all a test page needs.
The template
Here's app/templates/base.html:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Django + Tailwind</title>
<link rel="stylesheet" href="{% static 'output.css' %}">
</head>
<body class="p-8">
<h1 class="text-4xl font-bold text-red-500">Hello, world!</h1>
</body>
</html>
{% load static %} at the top makes the static tag available, and the stylesheet link uses it to point at output.css. Link the compiled file, not the globals.css you write. globals.css only holds an @import line that means nothing to a browser. I linked the wrong one at first in the video too.
The classes on the heading are only there so you can see at a glance that Tailwind is working.
The Tailwind input and the npm script
app/static/globals.css is the input file Tailwind reads, and it holds the one line from earlier:
@import "tailwindcss";
Add your own CSS below it if you need any. Then add two scripts to package.json, one for development and one for production:
{
"name": "tailwind_integration",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"css:watch": "tailwindcss -i app/static/globals.css -o app/static/output.css --watch",
"css:build": "tailwindcss -i app/static/globals.css -o app/static/output.css --minify"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@tailwindcss/cli": "^4.3.3",
"tailwindcss": "^4.3.3"
}
}
-i is the input file and -o is the output. --watch keeps the CLI running and rebuilds output.css every time you save a file that could contain classes, templates included. --minify makes the production file as small as possible.
Run it
You need two terminals, both in the project folder. In the first, start the Tailwind watcher:
npm run css:watch
It prints "Done" with a time in milliseconds and keeps running. bun run css:watch does the same thing.
In the second, start Django:
uv run python manage.py runserver
If you'd rather activate the virtual environment with source .venv/bin/activate, plain python manage.py runserver works too. Just make sure the Django terminal is using the right environment.
Open http://127.0.0.1:8000. You should see "Hello, world!" in large, bold red text with some padding around the page. Now add a class to the heading, say underline, and save. The watcher rebuilds the CSS in a few milliseconds, and when you refresh the page, the heading is underlined.
If the page errors or the text shows up unstyled, look for the two typos I made in the video. I forgot the closing %} on the static tag, and I wrote font-4xl instead of text-4xl. Tailwind silently ignores a class that doesn't exist, so a typo in a class name just means no style.
Also check that the watcher terminal is still running. The watcher stops when its input stream closes. In a terminal that never happens, but if you start it from a process manager or a container with no terminal attached, it exits straight away. Use --watch=always there to keep it running.
Before you deploy
In production, Django doesn't serve static files from your apps. collectstatic copies them all into one folder for your web server or a package like WhiteNoise to serve. Set where that folder goes in settings.py:
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
Then build the CSS before you collect:
npm run css:build
uv run python manage.py collectstatic --noinput
Order matters here. If collectstatic runs first, it copies whatever output.css was on disk, which could be stale or missing. Put both lines in your deploy script so the build server does it every time.
One more thing: the .gitignore that uv init creates only covers Python files, so add node_modules/ to it yourself.
Keep each tool doing its own job
uv handles the Python side and Bun handles the npm side, in the same repo, and neither gets in the other's way. Tailwind builds a CSS file and Django serves it, and that's all there is to it. You don't need a separate framework or an API for this. You keep writing Django templates and using every Django feature you already know, and Tailwind runs next to them.
Originally published on hassandev.me.
Top comments (0)