Jack's blog

Styling Django Forms

When you drop a {{ form }} tag into your template. By default it's going to look pretty awful:

Standard Recommendations

For these example I'm using Bootstrap 5, but these approaches aren't tied to a specific CSS framework (just change the styles to meet your needs).


Step 1: Add classes to widgets

class DebtForm(forms.ModelForm):
    class Meta:
        model = Debt
        fields = "__all__"
        widgets = {
            "name": forms.TextInput(
                attrs={
                    "class": "form-control",
                    "placeholder": "Debt Name",
                }
            ),
            "description": forms.Textarea(
                attrs={
                    "class": "form-control",
                    "placeholder": "Debt Description",
                }
            ),
            "value": forms.NumberInput(
                attrs={
                    "class": "form-control",
                    "placeholder": "Debt Value",
                }
            ),
        }

Because we've cheated and used bootstrap, things are immediately looking better, but there's some some issues:


Step 2. Utilize BoundField

We can take advantage of BuildField and proide our own implementation to add specific class attributes to the div and label tags.

See Customizing BoundField in the official docs

class StyledLabelBoundField(forms.BoundField):
    def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
        attrs = attrs or {}
        attrs["class"] = "form-label"
        return super().label_tag(contents, attrs, label_suffix, tag)

    def css_classes(self, extra_classes=None):
        parent_css_classes = super().css_classes(extra_classes)
        return f"mb-3 {parent_css_classes}".strip()

Then we can specify this in our form:

class DebtForm(forms.ModelForm):
    bound_field_class = StyledLabelBoundField

Our outputted HTML now looks much better:

<form method="post" class="mt-3">
   <div class="mb-3">
      <label class="form-label" for="id_name">Name:</label>
      <ul class="errorlist" id="id_name_error">
         <li>Invalid name.</li>
      </ul>
      <input type="text" name="name" value="test" class="form-control" placeholder="Debt Name" maxlength="100" required="" aria-invalid="true" aria-describedby="id_name_error" id="id_name">
   </div>
</form>

This improves the spacing between form elements:


Step 4. Automatically add validation classes

We can override the forms __init__ method to attach some styles to each fields widget.attrs depending on its validation status.

class DebtForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.data:
            for key, field in self.fields.items():
                if key in self.errors:
                    field.widget.attrs["class"] += " is-invalid"
                else:
                    field.widget.attrs["class"] += " is-valid"

We now get pretty styles for fields that have passed/failed validation:


Step 5. Configure FORM_RENDERER

If we look in Django debug toolbar we can see what templates are being used to render the form:

You can see from the screenshot Django is using the buitin: .venv/lib/python3.13/site-packages/django/forms/templates/django/forms/field.html template.

In order to override this template we'll need to modify the following setting in settings.py

FORM_RENDERER = "django.forms.renderers.TemplatesSetting"

To tell Django where to look for our templates we can update the TEMPLATES section in settings.py

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        ...

We'll also need to update INSTALLED_APPS to include django.forms:

INSTALLED_APPS = [
    ...
    "django.forms",
]

Step 6. Override Templates

We can copy the contents of the default django/forms/field.html template file into templates/django/forms/field.html in the root of our repository.

Likewise, can can provide our own implementation of templates/django/forms/errors/list/default.html to improve how errors are styled.

Our templates directory structure should look something like:

templates
└── django
   └── forms
      ├── errors
      │  └── list
      │     └── default.html
      └── field.html

templates/django/forms/field.html looks pretty identical to the default implementation, we've just swapped the {{ field }} and {{ field.errors }}

{% if field.use_fieldset %}
  <fieldset{% if field.aria_describedby %} aria-describedby="{{ field.aria_describedby }}"{% endif %}>
  {% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
  {% if field.label %}{{ field.label_tag }}{% endif %}
{% endif %}
{% if field.help_text %}<div class="helptext"{% if field.auto_id %} id="{{ field.auto_id }}_helptext"{% endif %}>{{ field.help_text|safe }}</div>{% endif %}
{{ field }}
{{ field.errors }}{% if field.use_fieldset %}</fieldset>{% endif %}

templates/django/forms/errors/list/default.html

{% if errors %}
<div class="invalid-feedback">
  {% for error in errors %}
  <p>{{ error }}</p>
  {% endfor %}
</div>
{% endif %}

Now our error messages are displayed underneath each inout and have lovely boostrap styling:



Before and After

Irrespective of which approach, the end result will look identical, here's a quick comparison

Alternatives

There's a bunch of alternative approaches you can take to style your forms in Django. Approaches aren't mutually exclusive, you can use a combination of techniques where it makes sense.


Rendering Fields Manually

The official Django Docs have a section on Rendering Fields Manually

If your application has a low number of forms, or many different forms with little styling overlap you might not actually benefit from abstracting out your rendering code in a DRY fashion.

This approach makes it super obvious what's going on in your template, but might be hard to keep things consistent as your application grows and you need to update form styling across your application.


Reusable Form templates

Another option detailed in the official docs is to utilize Reusable form templates

from django.forms.renderers import TemplatesSetting


class CustomFormRenderer(TemplatesSetting):
    form_template_name = "form_snippet.html"


FORM_RENDERER = "project.settings.CustomFormRenderer"

And then provide your own template to replace the default Django one:

# In form_snippet.html:
{% for field in form %}
    <div class="fieldWrapper">
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

Unlike the approach outlined in this article, which selectively overrides specific templates used within templates/django/forms/div.html. This approach essentially tells Django to use your form_snippet.html instead.


Django Crispy Forms

https://django-crispy-forms.readthedocs.io/en/latest

This popular package lets you utlize a template tag to provide fancy styling to your forms, saving you the work of having to do this manually.

{% load crispy_forms_tags %}

<form method="post" class="my-class">
    {{ my_formset|crispy }}
</form>