Django + HTMX + TailwindCSS Setup Guide: Complete 2025 Tutorial

by Nazmul H Khan, Co-Founder / CTO

Django + HTMX + TailwindCSS Setup Guide: Complete 2025 Tutorial

Building modern Django applications no longer means choosing between server-side rendering and complex JavaScript frameworks. With HTMX and TailwindCSS, you can create interactive, responsive web applications that combine Django's backend power with modern frontend experiences.

This comprehensive tutorial shows you how to set up a production-ready Django + HTMX + TailwindCSS stack that delivers 90% of SPA functionality with 10% of the complexity.

Why Django + HTMX + TailwindCSS in 2025?

The Perfect Trinity for Modern Web Development:

  • Django: Robust backend framework with 20+ years of proven architecture
  • HTMX: Modern interactivity without JavaScript complexity (1.8MB vs 45MB for React)
  • TailwindCSS: Utility-first styling that increases development speed by 50%

Key Advantages:

  • 60% faster development compared to Django + React setups
  • 85% smaller bundle sizes than traditional SPA applications
  • Better SEO performance with server-side rendering by default
  • Simpler deployment - no separate frontend build processes

What You'll Build in This Tutorial

By the end of this guide, you'll have:

  • ✅ A fully configured Django project with HTMX and TailwindCSS
  • ✅ Dynamic forms that submit without page refreshes
  • ✅ Real-time search and filtering capabilities
  • ✅ Modal windows and interactive components
  • ✅ Responsive design with modern UI patterns
  • ✅ Production-ready deployment configuration

Prerequisites and Environment Setup

Required Knowledge:

  • Python fundamentals and basic Django concepts
  • HTML/CSS basics (TailwindCSS knowledge helpful but not required)
  • Command line familiarity

System Requirements:

  • Python 3.9+ (recommended: Python 3.11+)
  • Node.js 16+ (for TailwindCSS compilation)
  • Git for version control

Time Investment: 2-4 hours for complete setup and examples

Step 1: Django Project Foundation

Create Your Django Project Structure

# Create project directory
mkdir django-htmx-tailwind-project
cd django-htmx-tailwind-project

# Set up Python virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install Django and essential packages
pip install django==5.0.2
pip install django-htmx==1.17.0
pip install python-dotenv==1.0.1
pip install django-browser-reload==1.12.1  # For development
pip install whitenoise==6.6.0  # For static file serving

# Create Django project
django-admin startproject myproject .
cd myproject

# Create your first app
python manage.py startapp core

Configure Django Settings

Update your settings.py file:

# settings.py
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

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

# Essential settings
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0']

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Third party apps
    'django_htmx',
    'django_browser_reload',  # Remove in production
    
    # Local apps
    'core',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # For static files
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django_htmx.middleware.HtmxMiddleware',  # HTMX middleware
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_browser_reload.middleware.BrowserReloadMiddleware',  # Development only
]

ROOT_URLCONF = 'myproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Database configuration
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# Static files configuration
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# HTMX Configuration
HTMX_CACHE_CONTROL = "no-cache, no-store, must-revalidate"

Step 2: TailwindCSS Integration and Configuration

Install and Configure TailwindCSS

# Initialize npm and install TailwindCSS
npm init -y
npm install -D tailwindcss@latest
npm install -D @tailwindcss/forms @tailwindcss/typography

# Generate TailwindCSS configuration
npx tailwindcss init

# Create directory structure for static files
mkdir -p static/css static/js

Configure TailwindCSS

Update your tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './templates/**/*.html',
    './core/templates/**/*.html',
    './static/js/**/*.js',
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        }
      },
      fontFamily: {
        'sans': ['Inter', 'system-ui', 'sans-serif'],
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography'),
  ],
}

Create TailwindCSS Input File

Create static/css/input.css:

/* static/css/input.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom component classes */
@layer components {
  .btn-primary {
    @apply bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200;
  }
  
  .btn-secondary {
    @apply bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition-colors duration-200;
  }
  
  .input-field {
    @apply block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500;
  }
  
  .card {
    @apply bg-white shadow-lg rounded-lg p-6 border border-gray-200;
  }
}

/* HTMX specific styles */
.htmx-indicator {
  opacity: 0;
  transition: opacity 200ms ease-in;
}

.htmx-request .htmx-indicator {
  opacity: 1;
}

.htmx-request.htmx-indicator {
  opacity: 1;
}

/* Loading states */
.loading {
  @apply opacity-50 pointer-events-none;
}

/* Smooth transitions for HTMX swaps */
.htmx-swapping {
  opacity: 0;
  transition: opacity 150ms ease-out;
}

Build Script Configuration

Add these scripts to your package.json:

{
  "scripts": {
    "build-css": "tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch",
    "build-css-prod": "tailwindcss -i ./static/css/input.css -o ./static/css/output.css --minify"
  }
}

Step 3: HTMX Integration and Configuration

Download and Configure HTMX

# Download HTMX library
curl -o static/js/htmx.min.js https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js

# Download HTMX extensions (optional but recommended)
curl -o static/js/htmx-preload.js https://unpkg.com/htmx.org@1.9.10/dist/ext/preload.js

Create Base Template

Create templates/base.html:

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en" class="h-full bg-gray-50">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Django + HTMX + TailwindCSS{% endblock %}</title>
    
    {% load static %}
    
    <!-- TailwindCSS -->
    <link href="{% static 'css/output.css' %}" rel="stylesheet">
    
    <!-- Google Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    
    <!-- HTMX -->
    <script src="{% static 'js/htmx.min.js' %}"></script>
    <script src="{% static 'js/htmx-preload.js' %}"></script>
    
    {% block extra_head %}{% endblock %}
</head>
<body class="h-full" hx-ext="preload">
    <!-- Navigation Bar -->
    <nav class="bg-white shadow-sm border-b border-gray-200">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex items-center">
                    <h1 class="text-xl font-semibold text-gray-900">
                        <a href="{% url 'core:home' %}">Django HTMX App</a>
                    </h1>
                </div>
                <div class="flex items-center space-x-4">
                    <a href="{% url 'core:home' %}" 
                       class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
                        Home
                    </a>
                    <a href="{% url 'core:tasks' %}" 
                       class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
                        Tasks
                    </a>
                </div>
            </div>
        </div>
    </nav>

    <!-- Main Content -->
    <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
        <!-- Messages -->
        <div id="messages" class="mb-4">
            {% if messages %}
                {% for message in messages %}
                    <div class="bg-{{ message.tags }}-100 border border-{{ message.tags }}-400 text-{{ message.tags }}-700 px-4 py-3 rounded relative mb-4"
                         x-data="{ show: true }" 
                         x-show="show" 
                         x-transition
                         x-init="setTimeout(() => show = false, 5000)">
                        {{ message }}
                    </div>
                {% endfor %}
            {% endif %}
        </div>

        <!-- Page Content -->
        <div class="px-4 py-6 sm:px-0">
            {% block content %}{% endblock %}
        </div>
    </main>

    <!-- HTMX Configuration -->
    <script>
        // Configure HTMX defaults
        htmx.config.globalViewTransitions = true;
        htmx.config.refreshOnHistoryMiss = true;
        
        // Global HTMX event handlers
        document.body.addEventListener('htmx:beforeSwap', function(evt) {
            if (evt.detail.xhr.status === 422) {
                // Allow 422 responses to swap as they likely contain form validation errors
                evt.detail.shouldSwap = true;
                evt.detail.isError = false;
            }
        });
        
        // Show loading indicators
        document.body.addEventListener('htmx:beforeRequest', function(evt) {
            const target = evt.target;
            target.classList.add('loading');
        });
        
        document.body.addEventListener('htmx:afterRequest', function(evt) {
            const target = evt.target;
            target.classList.remove('loading');
        });
    </script>
    
    {% block extra_js %}{% endblock %}
</body>
</html>

Step 4: Building Interactive Components with HTMX

Create Django Models

Create your models in core/models.py:

# core/models.py
from django.db import models
from django.urls import reverse

class Task(models.Model):
    PRIORITY_CHOICES = [
        ('low', 'Low'),
        ('medium', 'Medium'),
        ('high', 'High'),
    ]
    
    STATUS_CHOICES = [
        ('todo', 'To Do'),
        ('in_progress', 'In Progress'),
        ('completed', 'Completed'),
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='todo')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    completed_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('core:task_detail', kwargs={'pk': self.pk})
    
    @property
    def priority_color(self):
        colors = {
            'low': 'green',
            'medium': 'yellow', 
            'high': 'red'
        }
        return colors.get(self.priority, 'gray')
    
    @property
    def status_color(self):
        colors = {
            'todo': 'gray',
            'in_progress': 'blue',
            'completed': 'green'
        }
        return colors.get(self.status, 'gray')

Create Django Views with HTMX Support

Create comprehensive views in core/views.py:

# core/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse
from django.contrib import messages
from django.views.generic import ListView
from django.db.models import Q
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from .models import Task
from .forms import TaskForm

def home(request):
    """Home page with dashboard overview"""
    tasks = Task.objects.all()
    context = {
        'total_tasks': tasks.count(),
        'completed_tasks': tasks.filter(status='completed').count(),
        'in_progress_tasks': tasks.filter(status='in_progress').count(),
        'high_priority_tasks': tasks.filter(priority='high').count(),
    }
    return render(request, 'core/home.html', context)

class TaskListView(ListView):
    model = Task
    template_name = 'core/tasks.html'
    context_object_name = 'tasks'
    paginate_by = 10
    
    def get_queryset(self):
        queryset = Task.objects.all()
        
        # Search functionality
        search = self.request.GET.get('search')
        if search:
            queryset = queryset.filter(
                Q(title__icontains=search) | 
                Q(description__icontains=search)
            )
        
        # Filter by status
        status = self.request.GET.get('status')
        if status:
            queryset = queryset.filter(status=status)
            
        # Filter by priority  
        priority = self.request.GET.get('priority')
        if priority:
            queryset = queryset.filter(priority=priority)
            
        return queryset
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = TaskForm()
        context['current_search'] = self.request.GET.get('search', '')
        context['current_status'] = self.request.GET.get('status', '')
        context['current_priority'] = self.request.GET.get('priority', '')
        return context

@require_http_methods(["GET", "POST"])
def task_create(request):
    """Create new task with HTMX support"""
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            task = form.save()
            messages.success(request, f'Task "{task.title}" created successfully!')
            
            if request.htmx:
                # Return the new task card for HTMX requests
                return render(request, 'core/partials/task_card.html', {'task': task})
            else:
                return redirect('core:tasks')
        else:
            if request.htmx:
                return render(request, 'core/partials/task_form.html', 
                            {'form': form}, status=422)
    else:
        form = TaskForm()
    
    return render(request, 'core/partials/task_form.html', {'form': form})

@require_http_methods(["POST"])
def task_update_status(request, pk):
    """Update task status via HTMX"""
    task = get_object_or_404(Task, pk=pk)
    new_status = request.POST.get('status')
    
    if new_status in dict(Task.STATUS_CHOICES):
        task.status = new_status
        if new_status == 'completed':
            task.completed_at = timezone.now()
        else:
            task.completed_at = None
        task.save()
        
        messages.success(request, f'Task status updated to {task.get_status_display()}')
    
    if request.htmx:
        return render(request, 'core/partials/task_card.html', {'task': task})
    
    return redirect('core:tasks')

@require_http_methods(["DELETE"])
def task_delete(request, pk):
    """Delete task via HTMX"""
    task = get_object_or_404(Task, pk=pk)
    task_title = task.title
    task.delete()
    
    messages.success(request, f'Task "{task_title}" deleted successfully!')
    
    if request.htmx:
        return HttpResponse()  # Return empty response for HTMX to remove element
    
    return redirect('core:tasks')

def search_tasks(request):
    """Live search for tasks"""
    search_query = request.GET.get('search', '')
    tasks = Task.objects.all()
    
    if search_query:
        tasks = tasks.filter(
            Q(title__icontains=search_query) | 
            Q(description__icontains=search_query)
        )
    
    return render(request, 'core/partials/task_list.html', {
        'tasks': tasks,
        'search_query': search_query
    })

Create Django Forms

Create core/forms.py:

# core/forms.py
from django import forms
from .models import Task

class TaskForm(forms.ModelForm):
    class Meta:
        model = Task
        fields = ['title', 'description', 'priority', 'status']
        
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add CSS classes to form fields
        self.fields['title'].widget.attrs.update({
            'class': 'input-field',
            'placeholder': 'Enter task title...'
        })
        
        self.fields['description'].widget.attrs.update({
            'class': 'input-field',
            'rows': 3,
            'placeholder': 'Enter task description...'
        })
        
        self.fields['priority'].widget.attrs.update({
            'class': 'input-field'
        })
        
        self.fields['status'].widget.attrs.update({
            'class': 'input-field'
        })

Step 5: Creating Dynamic Templates with HTMX

Create Task Management Templates

Create templates/core/tasks.html:

<!-- templates/core/tasks.html -->
{% extends 'base.html' %}

{% block title %}Task Management - Django HTMX{% endblock %}

{% block content %}
<div class="space-y-6">
    <!-- Header -->
    <div class="flex justify-between items-center">
        <h2 class="text-2xl font-bold text-gray-900">Task Management</h2>
        <button hx-get="{% url 'core:task_create' %}" 
                hx-target="#task-form-modal" 
                hx-swap="innerHTML"
                class="btn-primary">
            Add New Task
        </button>
    </div>

    <!-- Search and Filters -->
    <div class="card">
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
            <!-- Live Search -->
            <div>
                <label for="search" class="block text-sm font-medium text-gray-700 mb-1">Search</label>
                <input type="text" 
                       name="search" 
                       id="search"
                       class="input-field"
                       placeholder="Search tasks..."
                       hx-get="{% url 'core:search_tasks' %}"
                       hx-trigger="keyup changed delay:500ms"
                       hx-target="#task-list"
                       hx-indicator="#search-spinner"
                       value="{{ current_search }}">
                <div id="search-spinner" class="htmx-indicator">
                    <div class="text-sm text-gray-500 mt-1">Searching...</div>
                </div>
            </div>
            
            <!-- Status Filter -->
            <div>
                <label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
                <select name="status" 
                        id="status" 
                        class="input-field"
                        hx-get="{% url 'core:tasks' %}"
                        hx-target="#task-list"
                        hx-trigger="change">
                    <option value="">All Statuses</option>
                    <option value="todo" {% if current_status == 'todo' %}selected{% endif %}>To Do</option>
                    <option value="in_progress" {% if current_status == 'in_progress' %}selected{% endif %}>In Progress</option>
                    <option value="completed" {% if current_status == 'completed' %}selected{% endif %}>Completed</option>
                </select>
            </div>
            
            <!-- Priority Filter -->
            <div>
                <label for="priority" class="block text-sm font-medium text-gray-700 mb-1">Priority</label>
                <select name="priority" 
                        id="priority" 
                        class="input-field"
                        hx-get="{% url 'core:tasks' %}"
                        hx-target="#task-list"
                        hx-trigger="change">
                    <option value="">All Priorities</option>
                    <option value="low" {% if current_priority == 'low' %}selected{% endif %}>Low</option>
                    <option value="medium" {% if current_priority == 'medium' %}selected{% endif %}>Medium</option>
                    <option value="high" {% if current_priority == 'high' %}selected{% endif %}>High</option>
                </select>
            </div>
        </div>
    </div>

    <!-- Task List -->
    <div id="task-list">
        {% include 'core/partials/task_list.html' %}
    </div>
    
    <!-- Task Form Modal -->
    <div id="task-form-modal"></div>
</div>
{% endblock %}

Create Partial Templates for HTMX

Create templates/core/partials/task_list.html:

<!-- templates/core/partials/task_list.html -->
<div class="space-y-4">
    {% if tasks %}
        {% for task in tasks %}
            {% include 'core/partials/task_card.html' %}
        {% endfor %}
    {% else %}
        <div class="text-center py-12">
            <div class="text-gray-500 text-lg">No tasks found</div>
            <div class="text-gray-400 text-sm mt-2">
                {% if search_query %}
                    Try adjusting your search criteria
                {% else %}
                    Create your first task to get started
                {% endif %}
            </div>
        </div>
    {% endif %}
</div>

Create templates/core/partials/task_card.html:

<!-- templates/core/partials/task_card.html -->
<div class="card hover:shadow-xl transition-shadow duration-200" id="task-{{ task.pk }}">
    <div class="flex justify-between items-start">
        <div class="flex-1">
            <div class="flex items-center space-x-2 mb-2">
                <h3 class="text-lg font-medium text-gray-900">{{ task.title }}</h3>
                
                <!-- Priority Badge -->
                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                           bg-{{ task.priority_color }}-100 text-{{ task.priority_color }}-800">
                    {{ task.get_priority_display }}
                </span>
                
                <!-- Status Badge -->
                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                           bg-{{ task.status_color }}-100 text-{{ task.status_color }}-800">
                    {{ task.get_status_display }}
                </span>
            </div>
            
            {% if task.description %}
                <p class="text-gray-600 mb-3">{{ task.description|truncatewords:20 }}</p>
            {% endif %}
            
            <div class="text-sm text-gray-500">
                Created: {{ task.created_at|date:"M d, Y" }}
                {% if task.completed_at %}
                    | Completed: {{ task.completed_at|date:"M d, Y" }}
                {% endif %}
            </div>
        </div>
        
        <!-- Action Buttons -->
        <div class="flex items-center space-x-2 ml-4">
            <!-- Status Update -->
            <select class="text-sm border-gray-300 rounded-md"
                    hx-post="{% url 'core:task_update_status' task.pk %}"
                    hx-target="#task-{{ task.pk }}"
                    hx-swap="outerHTML"
                    name="status">
                <option value="todo" {% if task.status == 'todo' %}selected{% endif %}>To Do</option>
                <option value="in_progress" {% if task.status == 'in_progress' %}selected{% endif %}>In Progress</option>
                <option value="completed" {% if task.status == 'completed' %}selected{% endif %}>Completed</option>
            </select>
            
            <!-- Delete Button -->
            <button class="text-red-600 hover:text-red-800 p-1"
                    hx-delete="{% url 'core:task_delete' task.pk %}"
                    hx-target="#task-{{ task.pk }}"
                    hx-swap="outerHTML"
                    hx-confirm="Are you sure you want to delete this task?">
                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                          d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                </svg>
            </button>
        </div>
    </div>
</div>

Create templates/core/partials/task_form.html:

<!-- templates/core/partials/task_form.html -->
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4" 
     id="modal-backdrop">
    <div class="bg-white rounded-lg p-6 max-w-md w-full">
        <div class="flex justify-between items-center mb-4">
            <h3 class="text-lg font-medium text-gray-900">Add New Task</h3>
            <button class="text-gray-400 hover:text-gray-600" 
                    onclick="document.getElementById('task-form-modal').innerHTML = ''">
                <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                </svg>
            </button>
        </div>
        
        <form hx-post="{% url 'core:task_create' %}" 
              hx-target="#task-list" 
              hx-swap="afterbegin"
              class="space-y-4">
            {% csrf_token %}
            
            <div>
                <label for="{{ form.title.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-1">
                    Title
                </label>
                {{ form.title }}
                {% if form.title.errors %}
                    <p class="text-red-600 text-sm mt-1">{{ form.title.errors.0 }}</p>
                {% endif %}
            </div>
            
            <div>
                <label for="{{ form.description.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-1">
                    Description
                </label>
                {{ form.description }}
                {% if form.description.errors %}
                    <p class="text-red-600 text-sm mt-1">{{ form.description.errors.0 }}</p>
                {% endif %}
            </div>
            
            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label for="{{ form.priority.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-1">
                        Priority
                    </label>
                    {{ form.priority }}
                    {% if form.priority.errors %}
                        <p class="text-red-600 text-sm mt-1">{{ form.priority.errors.0 }}</p>
                    {% endif %}
                </div>
                
                <div>
                    <label for="{{ form.status.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-1">
                        Status
                    </label>
                    {{ form.status }}
                    {% if form.status.errors %}
                        <p class="text-red-600 text-sm mt-1">{{ form.status.errors.0 }}</p>
                    {% endif %}
                </div>
            </div>
            
            <div class="flex justify-end space-x-3">
                <button type="button" 
                        class="btn-secondary"
                        onclick="document.getElementById('task-form-modal').innerHTML = ''">
                    Cancel
                </button>
                <button type="submit" class="btn-primary">
                    <span class="htmx-indicator">Creating...</span>
                    <span>Create Task</span>
                </button>
            </div>
        </form>
    </div>
</div>

<script>
// Close modal when clicking outside
document.getElementById('modal-backdrop').addEventListener('click', function(e) {
    if (e.target === this) {
        document.getElementById('task-form-modal').innerHTML = '';
    }
});
</script>

Step 6: URL Configuration and Routing

Create core/urls.py:

# core/urls.py
from django.urls import path
from . import views

app_name = 'core'

urlpatterns = [
    path('', views.home, name='home'),
    path('tasks/', views.TaskListView.as_view(), name='tasks'),
    path('tasks/create/', views.task_create, name='task_create'),
    path('tasks/<int:pk>/update-status/', views.task_update_status, name='task_update_status'),
    path('tasks/<int:pk>/delete/', views.task_delete, name='task_delete'),
    path('search/', views.search_tasks, name='search_tasks'),
]

Update main myproject/urls.py:

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('core.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += [path('__reload__/', include('django_browser_reload.urls'))]

Step 7: Database Setup and Initial Data

# Create and apply migrations
python manage.py makemigrations
python manage.py migrate

# Create superuser
python manage.py createsuperuser

# Collect static files
python manage.py collectstatic --noinput

Step 8: Development Workflow and Testing

Start Development Servers

You'll need two terminals for optimal development:

Terminal 1 - Django Server:

python manage.py runserver

Terminal 2 - TailwindCSS Watcher:

npm run build-css

Testing Your HTMX Integration

  1. Visit http://localhost:8000 to see your home page
  2. Navigate to http://localhost:8000/tasks/ for the task management interface
  3. Test these HTMX features:
    • Live search - Type in the search box and see results update without page refresh
    • Modal forms - Click "Add New Task" to see HTMX-powered modal
    • Status updates - Change task status with dropdown (updates via HTMX)
    • Real-time deletion - Delete tasks with confirmation dialog
    • Filter functionality - Use status and priority filters

Debugging HTMX Issues

Common issues and solutions:

  1. HTMX not working?

    • Check browser console for JavaScript errors
    • Verify HTMX is loaded: htmx object should exist in console
    • Ensure django_htmx.middleware.HtmxMiddleware is in settings
  2. Forms not submitting via HTMX?

    • Check for {% csrf_token %} in forms
    • Verify hx-post URL is correct
    • Check Django logs for 403 errors
  3. Styles not applying?

    • Ensure TailwindCSS watcher is running
    • Check that output.css is being generated
    • Verify template paths in tailwind.config.js

Step 9: Production Deployment Configuration

Environment Configuration

Create .env file:

SECRET_KEY=your-super-secret-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

Production Settings

Create myproject/settings/production.py:

# myproject/settings/production.py
from .base import *
import dj_database_url

# Security settings
DEBUG = False
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')

# Database
DATABASES = {
    'default': dj_database_url.parse(
        os.getenv('DATABASE_URL', f'sqlite:///{BASE_DIR / "db.sqlite3"}')
    )
}

# Static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# Security headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# Remove django_browser_reload from INSTALLED_APPS and MIDDLEWARE
INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django_browser_reload']
MIDDLEWARE = [mw for mw in MIDDLEWARE if 'django_browser_reload' not in mw]

Build Production Assets

# Build optimized CSS
npm run build-css-prod

# Collect static files
python manage.py collectstatic --noinput

Advanced HTMX Patterns and Best Practices

1. Error Handling and User Feedback

// Advanced HTMX error handling
document.body.addEventListener('htmx:responseError', function(evt) {
    // Show user-friendly error message
    const errorDiv = document.createElement('div');
    errorDiv.className = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
    errorDiv.textContent = 'Something went wrong. Please try again.';
    document.getElementById('messages').appendChild(errorDiv);
});

// Success animations
document.body.addEventListener('htmx:afterSwap', function(evt) {
    // Add success animation to newly added elements
    if (evt.detail.target.classList.contains('card')) {
        evt.detail.target.style.opacity = '0';
        evt.detail.target.style.transform = 'translateY(-10px)';
        setTimeout(() => {
            evt.detail.target.style.transition = 'all 0.3s ease';
            evt.detail.target.style.opacity = '1';
            evt.detail.target.style.transform = 'translateY(0)';
        }, 10);
    }
});

2. Performance Optimization

# View optimization with caching
from django.views.decorators.cache import cache_page
from django.core.cache import cache

@cache_page(60 * 5)  # Cache for 5 minutes
def search_tasks(request):
    search_query = request.GET.get('search', '')
    cache_key = f'search_tasks_{search_query}'
    
    tasks = cache.get(cache_key)
    if tasks is None:
        tasks = Task.objects.filter(
            Q(title__icontains=search_query) | 
            Q(description__icontains=search_query)
        )
        cache.set(cache_key, tasks, 300)  # Cache for 5 minutes
    
    return render(request, 'core/partials/task_list.html', {
        'tasks': tasks,
        'search_query': search_query
    })

3. Real-time Updates with WebSockets (Optional)

For real-time features, you can combine HTMX with Django Channels:

# Install Django Channels
pip install channels channels-redis

# consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class TaskConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_group_name = 'tasks_updates'
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    async def task_update(self, event):
        await self.send(text_data=json.dumps({
            'type': 'task_update',
            'task_id': event['task_id'],
            'html': event['html']
        }))

Troubleshooting Common Issues

HTMX Not Loading or Working

  1. Check JavaScript Console: Look for 404 errors when loading HTMX
  2. Verify Static Files: Ensure python manage.py collectstatic has been run
  3. Check Middleware: Confirm HtmxMiddleware is in MIDDLEWARE setting

TailwindCSS Styles Not Applying

  1. Check Build Process: Ensure TailwindCSS watcher is running
  2. Verify Configuration: Check content paths in tailwind.config.js
  3. Clear Cache: Try hard refresh (Ctrl+F5) to clear CSS cache

Form Validation Errors

  1. CSRF Tokens: Always include {% csrf_token %} in forms
  2. Status Codes: Return status 422 for validation errors to trigger HTMX swap
  3. Error Templates: Create proper error templates for form validation

Performance Issues

  1. Database Queries: Use select_related() and prefetch_related() for efficiency
  2. Caching: Implement Django's caching framework for expensive operations
  3. Static Files: Use CDN or proper static file serving in production

Next Steps and Advanced Features

Extending Your Django HTMX Application

  1. User Authentication: Add login/logout with HTMX-powered forms
  2. File Uploads: Implement HTMX file upload with progress bars
  3. Real-time Notifications: Integrate WebSockets for live updates
  4. API Integration: Connect external APIs with HTMX requests
  5. Advanced Filtering: Build complex filter interfaces
  6. Drag and Drop: Implement sortable task lists with HTMX

Recommended Libraries and Tools

Django Extensions:

  • django-crispy-forms - Better form rendering
  • django-tables2 - Advanced table functionality
  • django-filter - Complex filtering capabilities
  • django-rest-framework - API endpoints for mobile apps

Frontend Enhancements:

  • Alpine.js - Minimal JavaScript framework that works great with HTMX
  • Chart.js - Interactive charts and graphs
  • SortableJS - Drag and drop functionality

Conclusion: The Power of Django + HTMX + TailwindCSS

You've now built a complete Django + HTMX + TailwindCSS application that demonstrates:

Interactive user interfaces without complex JavaScript frameworks
Real-time search and filtering with minimal code
Modal forms and dynamic updates using HTMX
Professional styling with TailwindCSS utilities
Production-ready deployment configuration

Key Advantages of This Stack

  • Faster Development: 60% less code than traditional SPA approaches
  • Better Performance: Smaller bundle sizes and server-side rendering
  • Easier Maintenance: Single language (Python) for most logic
  • SEO Friendly: Server-side rendering by default
  • Progressive Enhancement: Works without JavaScript as fallback

Why Choose This Stack in 2025?

The Django + HTMX + TailwindCSS combination offers the perfect balance of modern user experience and development simplicity. You get the interactivity users expect without the complexity of managing separate frontend and backend applications.

This approach is ideal for:

  • Startups that need to move fast with limited resources
  • Small to medium businesses building internal tools
  • Teams that prefer Python over JavaScript for most logic
  • Projects that prioritize simplicity and maintainability

Need Help Building Your Django HTMX Application?

At Sparrow Studio, we specialize in building modern Django applications with HTMX and TailwindCSS. Our team has helped 30+ companies build fast, interactive web applications using this powerful stack.

Our Django Development Services

Full-Stack Django Development

  • Django + HTMX applications: Interactive web apps without JavaScript complexity
  • TailwindCSS integration: Modern, responsive design systems
  • Database optimization: PostgreSQL, Redis, and performance tuning
  • API development: REST and GraphQL APIs for mobile and integrations

Django HTMX Consulting

  • Architecture review: Optimize your existing Django applications
  • Performance optimization: Speed up your Django + HTMX applications
  • Team training: Get your developers up to speed on HTMX patterns
  • Code review: Ensure best practices and maintainability

Why Choose Sparrow Studio for Django Development?

Django experts: 8+ years of Django development experience
HTMX specialists: Early adopters with production experience
Modern tooling: TailwindCSS, Alpine.js, and performance optimization
Startup experience: Fast delivery and cost-effective solutions
Full-stack capability: Frontend, backend, and deployment expertise

Ready to Build Your Django Application?

Schedule a free Django consultation → to discuss your project requirements, or explore our Django portfolio → to see real-world implementations.

What you'll get in your consultation:

  • Architecture recommendations for your use case
  • Technology stack advice and best practices
  • Project timeline and cost estimates
  • Performance and scalability planning

No sales pressure. No long-term commitments. Just expert advice about building modern Django applications.

More articles

AI 2027: The Most Compelling Forecast of Humanity's AI Future

What happens when artificial intelligence becomes smarter than humans? A groundbreaking scenario maps our potential path to superintelligence—and it's closer than you think. Explore the month-by-month journey to AI superintelligence by 2027.

Read more

Top Full Stack Development Studios USA 2025: Complete Comparison

Compare the best full stack development studios in USA: pricing, expertise, portfolios, and client reviews. Find the perfect development partner for your project in 2025.

Read more
💬 Let's Talk

Tell us about
your project

Ready to transform your ideas into reality? Let's discuss your project and how we can help you achieve your goals.

Our offices

  • Sheridan
    1309 Coffeen Avenue STE 1200
    Sheridan, WY 82801