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
- Visit
http://localhost:8000
to see your home page - Navigate to
http://localhost:8000/tasks/
for the task management interface - 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:
-
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
-
Forms not submitting via HTMX?
- Check for
{% csrf_token %}
in forms - Verify
hx-post
URL is correct - Check Django logs for 403 errors
- Check for
-
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
- Check JavaScript Console: Look for 404 errors when loading HTMX
- Verify Static Files: Ensure
python manage.py collectstatic
has been run - Check Middleware: Confirm
HtmxMiddleware
is inMIDDLEWARE
setting
TailwindCSS Styles Not Applying
- Check Build Process: Ensure TailwindCSS watcher is running
- Verify Configuration: Check
content
paths intailwind.config.js
- Clear Cache: Try hard refresh (Ctrl+F5) to clear CSS cache
Form Validation Errors
- CSRF Tokens: Always include
{% csrf_token %}
in forms - Status Codes: Return status 422 for validation errors to trigger HTMX swap
- Error Templates: Create proper error templates for form validation
Performance Issues
- Database Queries: Use
select_related()
andprefetch_related()
for efficiency - Caching: Implement Django's caching framework for expensive operations
- Static Files: Use CDN or proper static file serving in production
Next Steps and Advanced Features
Extending Your Django HTMX Application
- User Authentication: Add login/logout with HTMX-powered forms
- File Uploads: Implement HTMX file upload with progress bars
- Real-time Notifications: Integrate WebSockets for live updates
- API Integration: Connect external APIs with HTMX requests
- Advanced Filtering: Build complex filter interfaces
- Drag and Drop: Implement sortable task lists with HTMX
Recommended Libraries and Tools
Django Extensions:
django-crispy-forms
- Better form renderingdjango-tables2
- Advanced table functionalitydjango-filter
- Complex filtering capabilitiesdjango-rest-framework
- API endpoints for mobile apps
Frontend Enhancements:
Alpine.js
- Minimal JavaScript framework that works great with HTMXChart.js
- Interactive charts and graphsSortableJS
- 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.