Using Django’s template loaders to configure Tailwind

Tailwind has a config option to tell it where your HTML is:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],  // <--- Which files to scan?
  theme: {
    extend: {},
  plugins: [],

So for a basic Django project you might think it’s going to look something like this:

    content: [
        '../../templates/**/*.html',  // Project level templates folder.
        '../../../**/templates/**/*.html',  // App template folders.

That double ../.. is bad enough: you’re backing out of a project level static/css folder where you’ve got your tailwind.config.js. (“I don’t do it like that”, you scream — it’s only an example!)

You have nested folders, for includes and whatnot.

Then you have third-party apps which might be shipping templates too — are you really adding paths to you venv’s site-packages?

Frankly it’s a pain.

What you want is Django to tell Tailwind where your templates are. It knows.

A list_templates command

First you need to list all templates. Django can do that:

import os

from django.conf import settings
from import BaseCommand
from django.template.utils import get_app_template_dirs

class Command(BaseCommand):
    help = "List all template files"

    def handle(self, *args, **options):
        template_files = []
        app_template_dirs = get_app_template_dirs("templates")
        for app_template_dir in app_template_dirs:
            template_files += self.list_template_files(app_template_dir)
        for template_dir in settings.TEMPLATES[0]["DIRS"]:
            template_files += self.list_template_files(template_dir)

    def list_template_files(self, template_dir):
        template_files = []
        # TODO: Look into using pathlib.Path.rglob() instead. 🤔
        for dirpath, _, filenames in os.walk(str(template_dir)):
            for filename in filenames:
                if filename.endswith(".html") or filename.endswith(".txt"):
                    template_files.append(os.path.join(dirpath, filename))
        return template_files

This would be a management command, so somewhere like myapp/management/commands/

Telling Tailwind

Then we need to use that in our Tailwind config.

Something like this in your tailwind.config.js:

// Resolve path to directory containing file.
// This is the root of the project.
// Then assumed layout of <main-app>/static/css/tailwind.config.js, so up 3 levels.
// Adjust for your needs.
const path = require('path');
const projectRoot = path.resolve(__dirname, '../../..');

const { spawnSync } = require('child_process');

// Function to execute the Django management command and capture its output
const getTemplateFiles = () => {
  const command = 'python'; // Requires virtualenv to be activated.
  const args = ['', 'list_templates']; // Requires cwd to be set.
  const options = { cwd: projectRoot };
  const result = spawnSync(command, args, options);

  if (result.error) {
    throw result.error;

  if (result.status !== 0) {
    console.log(result.stdout.toString(), result.stderr.toString());
    throw new Error(`Django management command exited with code ${result.status}`);

  const templateFiles = result.stdout.toString()
    .map((file) => file.trim())
    .filter(function(e){return e});  // Remove empty strings, including last empty line.
  return templateFiles;

module.exports = {
  // Allow configuring some folders manually, and then concatenate with the
  // output of the Django management command.
  content: [].concat(getTemplateFiles()),
  theme: {
    extend: {},
  plugins: [],
// console.log(module.exports)

Enough messing around. Enjoy! 🚀