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:
- The labels are missing the right class
- There's no padding between form elements
- Error/validation messages are styled incorrectly
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>
- Each
div
now has aclass="mb-3"
- Each
label
tag now has aclass="form-label"
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>