uptime · 1404 days · 23 posts published · last deploy 1 day, 10 hours ago build:passing rss
~ / software-web / tailwind-css-daisyui-in-wagtail-django-pragmatic-setup.md
Software & Web · 21. May 2026 · ~18min · f96af3e

Tailwind CSS + daisyUI in Wagtail/Django: Pragmatic Setup Without the Node Nightmare

Build Pipeline, collectstatic Integration and why daisyUI made 95% of my CSS redundant

>
devmaker.net
author · f96af3e · 2026-05-21
x
a-dark-moody-developer-workspace-close-up-of-a-c.jpg 1024×1024
a-dark-moody-developer-workspace-close-up-of-a-c
Image generated with AI
Wagtail doesn't come with a frontend opinion – and at some point you find yourself writing the same card component in custom CSS for the third time. Tailwind v4 with daisyUI as a component layer was my compromise: no Bootstrap override madness, no building your own CSS library from scratch, no interference with the Django static workflow. This article walks through the concrete setup for devmaker.net – with npm watch in dev, a multi-stage Docker build in production, and the real pitfalls I ran into along the way.

## Why use Tailwind in Wagtail at all?

Wagtail doesn't come with any frontend opinions out of the box. You get base.html, a bit of demo CSS, and that's it. That's fine – but at some point you find yourself writing your own card component, your own button class, your own modal for the third time. That's exactly where I was with devmaker.net.

Here is the translation: The honest assessment: I wanted **no** Bootstrap (too much overriding), **no** CSS-from-scratch (too much maintenance), and I wanted to **mess with nothing** in the Django static pipeline workflow that has been working for years. Tailwind v4 with daisyUI as the component layer was the compromise that ended up sticking.

Deliberately pragmatic, not over-engineered

No Vite, no Webpack, no dedicated npm server. Just Tailwind CLI + a npm run watch in dev and a one-time build in the Docker image. That's all you need.

Stack at a glance:

  • Wagtail 6.x on Django 5.x
  • Tailwind CSS v4 via @tailwindcss/cli (no more PostCSS fiddling required in v4)
  • daisyUI 5 as a component plugin
  • Django ManifestStaticFilesStorage for Cache Busting
  • Build in the Docker image using a multi-stage build (Node stage → Python stage)

Here is the translation: The idea: Tailwind generates one CSS file. It is treated like any other static file. Django/Wagtail know nothing about Node. That's exactly how it should be.

"Project Structure"

Here is the translation:

myproject/
├── frontend/                    # All Node stuff isolated
│   ├── package.json
│   ├── tailwind.config.js       # Optional in v4 – needed for daisyUI themes
│   └── src/
│       └── input.css            # Entry point with @import "tailwindcss"
├── myproject/
│   ├── settings/
│   ├── static_src/              # Global static files
│   └── templates/
│       └── base.html
├── static/                      # Build output from Tailwind goes here
│   └── css/
│       └── app.css              # ← gets collectstatic-ed by Django/Wagtail
├── manage.py
└── Dockerfile

Could you please provide the text you would like me to translate? It seems like only the title "package.json – minimal" was included, but no actual text to translate. Please share the full text and I'll be happy to help!

Here is the translated text:

{
  "name": "devmaker-frontend",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "build": "tailwindcss -i ./src/input.css -o ../static/css/app.css --minify",
    "watch": "tailwindcss -i ./src/input.css -o ../static/css/app.css --watch"
  },
  "devDependencies": {
    "@tailwindcss/cli": "^4.1.0",
    "daisyui": "^5.0.0",
    "tailwindcss": "^4.1.0"
  }
}

---

**Note:** This text is a **JSON configuration file** (specifically a `package.json` file for a Node.js/frontend project). It is **not a natural language text** and therefore **does not require translation**. The content consists of:

- **Technical keys and values** (e.g., `"name"`, `"version"`, `"scripts"`)
- **Command-line instructions** (e.g., `tailwindcss -i ./src/input.css ...`)
- **Dependency names and version numbers** (e.g., `"tailwindcss": "^4.1.0"`)

All of these are **language-neutral technical identifiers** that remain identical in any language context. The file has been reproduced exactly as provided, as any modification would alter its functionality.

input.css – the heart of it all

Tailwind v4 has tidied up the config workflow nicely. Instead of tailwind.config.js, most things can be declared directly in the CSS file via the @theme block. daisyUI is added as a @plugin:

Here is the translation of the text into English:

@import "tailwindcss";

/* Load daisyUI as a plugin, with two themes (light/dark) */
@plugin "daisyui" {
  themes: light --default, dark --prefersdark;
  root: ":root";
  logs: false;
}

/* Scan Wagtail templates AND Python files.
   Important: Wagtail snippets/StreamFields render templates
   that Tailwind would otherwise not find. */
@source "../../**/templates/**/*.html";
@source "../../**/*.py";

/* Custom theme variables – overrides daisyUI defaults */
@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --color-brand: oklch(70% 0.18 200);
}

/* Custom component layer for recurring Wagtail patterns */
@layer components {
  .prose-article {
    @apply prose prose-invert max-w-none
           prose-headings:font-bold
           prose-code:text-brand prose-code:bg-base-200
           prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded;
  }
}
@source is the key

Tailwind needs to know which files to scan for classes. Don't forget the Python files – if you, like me, set classes in Wagtail block templates or in get_context() methods, they need to be included in the scan, otherwise the classes will get purged.

Django Settings

Here, nothing spectacular happens – and that's exactly the point. Tailwind writes to static/css/app.css, Django picks it up via STATICFILES_DIRS, collectstatic distributes it. Done.

Here is the translation of the text into English:

# settings/base.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

STATICFILES_DIRS = [
    BASE_DIR / "static",          # ← Tailwind output goes here
    BASE_DIR / "myproject" / "static_src",
]

# Cache-busting for CSS/JS – important, otherwise visitors will see old CSS
STORAGES = {
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
    },
}

Please provide the text you would like me to translate. You only gave me a label/description ("base.html – einbinden mit data-theme") but no actual text to translate. However, if this short phrase itself is what needs to be translated, here it is: **"base.html – include with data-theme"**

Here is the translated text. Please note that this is an HTML/Django template code, which is largely language-agnostic. The structural and technical parts remain unchanged, as they are programming syntax. Only the visible text content has been translated:

```html
{% load static wagtailcore_tags %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" data-theme="dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block title %}{{ page.title }}{% endblock %}</title>

  <link rel="stylesheet" href="{% static 'css/app.css' %}">
</head>
<body class="min-h-screen bg-base-100 text-base-content antialiased">
  <header class="navbar bg-base-200 shadow-sm">
    <div class="flex-1">
      <a href="/" class="btn btn-ghost text-xl">devmaker.net</a>
    </div>
  </header>

  <main class="container mx-auto px-4 py-8">
    {% block content %}{% endblock %}
  </main>
</body>
</html>
```

**Note:** This is a Django/Wagtail HTML template. The code itself does not require translation, as it consists of:
- **HTML markup** (universal)
- **Django template tags** (programming syntax)
- **CSS class names** (technical identifiers)
- **The website name** `devmaker.net` (a proper noun, unchanged)

No translatable natural language text was found in this template.

Dev Workflow: two terminals, done

I deliberately don't use `django-tailwind` or similar wrapper packages. They add an abstraction layer that, in 80% of cases, just gets in the way. My workflow:

# Terminal 1: Tailwind in watch mode
cd frontend
npm run watch

# Terminal 2: Django runserver
python manage.py runserver

# Once during initial setup:
cd frontend && npm install

Tailwind automatically detects changes in templates and Python files and rebuilds the CSS in <200ms. Browser reloading is handled by the browser itself or a simple live-reload plugin – I don't need HMR for my use case.

Production: Multi-Stage Docker Build

Translate the following text into English, ensuring that the translated text has the same meaning as the original text: The production build needs to accomplish two things: use Node only at build time (not in the runtime image) and get the finished CSS into collectstatic.

Here is the translation of the text into English:

# ---------- Stage 1: Tailwind Build ----------
FROM node:22-alpine AS frontend-build
WORKDIR /app/frontend

COPY frontend/package*.json ./
RUN npm ci --no-audit --no-fund

# We need the templates AND Python files for scanning
COPY frontend/ ./
COPY myproject/ /app/myproject/
COPY static/ /app/static/

RUN npm run build

# ---------- Stage 2: Python Runtime ----------
FROM python:3.13-slim AS runtime
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 && rm -rf /var/lib/apt/lists/*

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Source code
COPY . .

# Pull in the finished Tailwind CSS from Stage 1
COPY --from=frontend-build /app/static/css/app.css /app/static/css/app.css

# Django collectstatic – now with Tailwind included
RUN python manage.py collectstatic --noinput

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
## Why no Tailwind in the Runtime Image?

Node + node_modules are ~300 MB. You don't want that in your production image. With Multi-Stage, only the ~60 KB CSS file remains.

daisyUI in Wagtail templates

Here, daisyUI really pays off. Instead of building a Tailwind class Lego for each card first, you write class="card bg-base-200" and you're done. Example of a topic card template:

Here is the translated text:

{# templates/blog/blocks/topic_card.html #}
{% load wagtailcore_tags wagtailimages_tags %}

<article class="card bg-base-200 shadow-md hover:shadow-xl transition-shadow">
  {% if page.promo_img %}
    {% image page.promo_img fill-800x400 class="card-img-top rounded-t-2xl" %}
  {% endif %}
  <div class="card-body">
    <h2 class="card-title">
      <a href="{% pageurl page %}" class="link link-hover">{{ page.title }}</a>
    </h2>
    <p class="text-base-content/70">{{ page.summary }}</p>
    <div class="card-actions justify-end mt-4">
      {% for tag in page.tags.all %}
        <span class="badge badge-outline badge-sm">{{ tag }}</span>
      {% endfor %}
    </div>
  </div>
</article>

**Note:** The provided text is a **Django/Wagtail HTML template** containing code, template tags, and CSS class names. Since this is **source code**, it does not require translation — all technical identifiers, template syntax, and class names must remain unchanged to preserve functionality. There is no natural language content in this template that would need to be translated from German to English.

Theme Switcher with Alpine.js (optional)

daisyUI uses the `data-theme` attribute. A switcher only needs one line of JS – I'm using Alpine because it's lightweight and fits well with the Wagtail philosophy:

Here is the translated text:

<div x-data="{ theme: localStorage.getItem('theme') || 'dark' }"
     x-init="document.documentElement.setAttribute('data-theme', theme)">
  <button class="btn btn-ghost btn-circle"
          @click="theme = theme === 'dark' ? 'light' : 'dark';
                  document.documentElement.setAttribute('data-theme', theme);
                  localStorage.setItem('theme', theme)">
    <span x-show="theme === 'dark'">☀️</span>
    <span x-show="theme === 'light'">🌙</span>
  </button>
</div>

**Note:** The provided text is **HTML/JavaScript code**, not a natural language text. Therefore, **no translation is necessary or possible**, as programming code is language-independent and universally written in English syntax. The code remains identical in its original form, as translating it would alter its functionality.

If you meant to translate a **description** of this code, please provide the text you would like translated, and I will be happy to assist.

# Lessons Learned (the real ones)

  1. Definitely check your @source paths. My first build had 800 KB of CSS because I had accidentally scanned node_modules. With the correct paths, I ended up at 58 KB minified.
  2. ManifestStaticFilesStorage + Tailwind @imports only work together if Tailwind first compiles everything into one CSS file. The CLI does this out of the box – but anyone working with multiple output files will run into hashing issues.
  3. daisyUI themes override Tailwind colors. If you use bg-blue-500 and daisyUI defines --color-primary, daisyUI doesn't win – but semantic classes like bg-primary are more fun in the long run.
  4. Not wanting to style the Wagtail admin. Tempting, but not worth the effort. Leave the Wagtail admin alone.
  5. Watch mode in WSL2 is tricky. If your frontend folder is located on a Windows mount, file watching does not work reliably. Solution: Put the code in the WSL filesystem (~/projects/).

**What I Left Out**

  • Vite/Webpack: I don't need this for an SSR Wagtail site. My JavaScript usage is minimal (Alpine.js via CDN is sufficient).
  • django-tailwind / django-cotton: Nice packages, but for my setup an additional layer without added value.
  • PostCSS Plugins: Tailwind v4 has Autoprefixer and Nesting built in. I no longer need my own PostCSS config.
  • Tailwind UI / Catalyst: Licensing costs I don't want to spend on a hobby project. daisyUI covers 90%, the rest is custom code.

## Conclusion & Outlook

Here is the translation: The combination of Tailwind v4 + daisyUI is currently the best compromise for Wagtail/Django in terms of **build speed**, **maintainability**, and **no-frontend-framework-brainfuck**. I've used it on devmaker.net to replace a good 800 lines of custom CSS with utility classes + daisyUI components – and can now think about content again instead of CSS spaghetti.

Planned follow-up articles:

  • StreamField blocks with custom daisyUI-based templates (Callout, Card, Stats)
  • # HTMX in Wagtail – List Filters Without Page Reload
  • # The Frontend Build in GitLab CI: Caching Strategies for npm and Tailwind

Here is the translation: If you have questions about specific edge cases (especially multi-site, i18n, and CSP headers with inline styles), write them in the comments.

// responses (0)
> echo "your thoughts" >> tailwind-css-daisyui-in-wagtail-django-pragmatic-setup.responses

Post your comment

Required for comment verification