Render Django Form with Tailwind CSS Style

Michael Yin

Last updated on November 24 2021

Table of Contents

Django Tailwind CSS, Alpine.js Tutorial Series:

  1. Introduction
  2. How to Setup Tailwind CSS with Django (Part 1)
  3. How to Setup Tailwind CSS with Django (Part 2)
  4. Optimize Tailwind CSS in Django
  5. Render Django Form with Tailwind CSS Style
  6. Integrate Alpine.js with Django (Part 1) (coming soon)
  7. Integrate Alpine.js with Django (Part 2) (coming soon)
  8. Build Task List with Tailwind CSS, Alpine.js and Django (coming soon)
  9. Django Form Validation in Tailwind Modal (Alpine.js) (coming soon)
  10. Django Form Validation in Tailwind Modal (Alpine.js + HTMX) (coming soon)
  11. How to deploy Django Tailwind CSS project with Docker (coming soon)

The source code is on Github/django-tailwind-alpine-htmx, demo is on Heroku

Objectives

By the end of this chapter, you should be able to:

  1. Render Django form with django-crispy-forms and crispy-tailwind
  2. Use tailwindcss/forms plugin to reset form style.

Tasks App

Let's first create django app tasks

(env)$ python manage.py startapp tasks
.
├── db.sqlite3
├── django_tailwind_app
├── env
├── frontend
├── manage.py
├── requirements.txt
└── tasks                   # new

Add tasks to the INSTALLED_APPS in django_tailwind_app/settings.py

INSTALLED_APPS = [
    ...
    'tasks',                  # new
]

Model

Update tasks/models.py

from django.db import models
from django.utils import timezone


class Task(models.Model):
    title = models.CharField(max_length=250)
    due_date = models.DateField(default=timezone.now)

Migrate the db

(env)$ python manage.py makemigrations
(env)$ python manage.py migrate

Form

Create tasks/forms.py

from django import forms
from .models import Task


class TaskForm(forms.ModelForm):

    class Meta:
        model = Task
        fields = ("title", "due_date")

View

Update tasks/views.py

from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render
from .forms import TaskForm


def create_task(request):
    if request.method == 'POST':
        form = TaskForm(request.POST)
        if form.is_valid():
            form.save()

            return HttpResponse('ok')
    else:
        form = TaskForm()

    return render(request, 'tasks/task_create.html', {'form': form})

Template

Create django_tailwind_app/templates/base.html

{% load webpack_loader %}

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {% stylesheet_pack 'app' %}
</head>
<body>

{% block content %}
{% endblock content %}

{% javascript_pack 'app' %}

</body>
</html>

Create django_tailwind_app/templates/tasks/task_create.html

{% extends "base.html" %}

{% block content %}

  <div class="w-full max-w-7xl mx-auto px-4">
    <form method="post">
      {% csrf_token %}

      {{ form.as_p }}

      <button type="submit" class="btn-blue"  value="Submit">Submit</button>
    </form>
  </div>

{% endblock %}

URL

Create tasks/urls.py

from django.urls import path

from .views import (
    create_task
)

urlpatterns = [
    path("create/", create_task, name="task-create"),
]

Create django_tailwind_app/urls.py

from django.contrib import admin
from django.urls import include, path
from django.views.generic import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name="index.html")),
    path("tasks/", include("tasks.urls")),
    path('admin/', admin.site.urls),
]

Manual Test

(env)$ python manage.py runserver

If we check http://127.0.0.1:8000/tasks/create/

As you can see, even we import Tailwind to our Django project, the default form style still look ugly.

Next, let's start improving the form style.

tailwindcss-forms

tailwindcss/forms is a plugin that provides a basic reset for form styles that makes form elements easy to override with utilities.

$ cd frontend
$ npm install @tailwindcss/forms

Update frontend/tailwind.config.js

const Path = require("path");
const pwd = process.env.PWD;

const purgePaths = [
  Path.join(pwd, "../django_tailwind_app/templates/**/*.html"),
];

console.log(`tailwindcss purge by scanning ${purgePaths}`);

module.exports = {
  mode: 'jit',
  purge: purgePaths,
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),                // new
  ],
}

crispy-tailwind

crispy-tailwind is a Tailwind template pack for django-crispy-forms

Update requirements.txt

django-crispy-forms==1.13.0        # new
crispy-tailwind==0.5.0             # new
$ pip install -r requirements.txt

Update django_tailwind_app/settings.py

INSTALLED_APPS = [

    "crispy_forms",                     # new
    "crispy_tailwind",                  # new
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"

CRISPY_TEMPLATE_PACK = "tailwind"

Update django_tailwind_app/templates/tasks/task_create.html

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}

  <div class="w-full max-w-7xl mx-auto px-4">
    <form method="post">
      {% csrf_token %}

      {{ form|crispy }}

      <button type="submit" class="btn-blue"  value="Submit">Submit</button>
    </form>
  </div>

{% endblock %}

Notes:

  1. We load crispy_forms_tags at the top
  2. {{ form|crispy }} will render the form using Tailwind template pack. (set by CRISPY_TEMPLATE_PACK)

Purge

If we check the elements in the devtools, we notice some tailwind css such as mb-2 is not working.

Why does that happen?

Because in the previous chapter, we enabled the jit mode in frontend/tailwind.config.js

Let's check it

const Path = require("path");
const pwd = process.env.PWD;

const purgePaths = [
  Path.join(pwd, "../django_tailwind_app/templates/**/*.html"),
];

console.log(`tailwindcss purge by scanning ${purgePaths}`);

module.exports = {
  mode: 'jit',
  purge: purgePaths,
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

So we should config purge to tell PurgeCSS to also scan the crispy-tailwind package as well to resolve this issue.

If you use other 3-party packages to manipulate tailwind css classnames, you should also do it.

Update frontend/tailwind.config.js

const Path = require("path");
const pwd = process.env.PWD;
const pySitePackages = process.env.pySitePackages;

// We can add current project paths here
const projectPaths = [
  Path.join(pwd, "../django_tailwind_app/templates/**/*.html"),
  // add js file paths if you need
];

// We can add 3-party python packages here
let pyPackagesPaths = []
if (pySitePackages){
  pyPackagesPaths = [
    Path.join(pySitePackages, "./crispy_tailwind/**/*.html"),
    Path.join(pySitePackages, "./crispy_tailwind/**/*.py"),
    Path.join(pySitePackages, "./crispy_tailwind/**/*.js"),
  ];
}

const purgePaths = [...projectPaths, ...pyPackagesPaths];
console.log(`tailwindcss purge by scanning ${purgePaths}`);

module.exports = {
  mode: 'jit',
  purge: purgePaths,
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

If we set pySitePackages when running npm run start, PurgeCSS will scan the files.

(env)$ python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))"
/Users/michaelyin/django_tailwind_project/env/lib/python3.9/site-packages

# set it to pySitePackages ENV variable
(env)$ export pySitePackages=$(python3 -c "import sysconfig; print(sysconfig.get_path('purelib'))")
# check
(env)$ env | grep pySitePackages
(env)$ cd frontend
(env)$ npm run start

As you can see, now the margin under the input element is working!

Django Tailwind CSS, Alpine.js Tutorial Series:

  1. Introduction
  2. How to Setup Tailwind CSS with Django (Part 1)
  3. How to Setup Tailwind CSS with Django (Part 2)
  4. Optimize Tailwind CSS in Django
  5. Render Django Form with Tailwind CSS Style
  6. Integrate Alpine.js with Django (Part 1) (coming soon)
  7. Integrate Alpine.js with Django (Part 2) (coming soon)
  8. Build Task List with Tailwind CSS, Alpine.js and Django (coming soon)
  9. Django Form Validation in Tailwind Modal (Alpine.js) (coming soon)
  10. Django Form Validation in Tailwind Modal (Alpine.js + HTMX) (coming soon)
  11. How to deploy Django Tailwind CSS project with Docker (coming soon)

The source code is on Github/django-tailwind-alpine-htmx, demo is on Heroku


Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.


Table of Contents

Frontend Guide For Python Dev

This FREE guide help Python developers to learn the Modern frontend tech

Learn More

Build Jamstack web app with Next.js and Wagtail CMS.

Read More

Subscribe

Get notified about new great Web Development Tutorial