Style forms in python


I have found myself having to recreate the same form several times. As soon as a it is a bit more complicated than form.as_p or table etc allows I find my self working in a very un DRY way.

Example: Name and Address fields. If you want it look like:
“First Name” “Last Name”
“zip code” “city”

or similar. 2 form fields sharing a line and adding some css to them to control length.

Is this possible to do in python. I would like to do things in python, because if I add a model field and update the form, every time that form is used gets auto updated.

I realize I could probably use a lot of html snippets, but I have been working on keeping the html down to a minimum.

Using the idea of html snippets and the include tag is the key toward creating reusable template fragments. Take a look at how Django itself uses multiple templates when rendering forms and fields. The way that Django has designed their template engine makes it desirable to create a larger number of smaller template files as “components”.
(Side note: This approach also makes it easier to start adding features that rely upon rendering page fragments, such as AJAX/HTMX, because you already have templates that only render the html that you want, and so can create views to only render those fragments.)

However, if you want more direct programmatic control over the contents and layout of forms, take a look at Django Crispy forms, particularly the sections on layouts and Updating layouts on the go.
(We rely a lot on Crispy forms.)

I might have to be more careful with context object names to use smaller snippets but might be the way.

I will have another look into crispy forms, when I looked at it previously, it seemed to be mostly bootstrap and I want to avoid the large css frameworks.

When digging into this a bit more. If I use vanilla django and just a bit of simple html and css and want to make something like below.

Can I get django to add the label and errors automatically or do I need to do it by hand as soon as I want to control a bit how it looks? For this example it is more or less only the zip-code/city combination that requires this additional control for the rest {{form.as_div}} would achieve the same result.

<div class="grid-2-col">
    <div class="grid-2-col form-address">
    <div class="grid-wide">{{client.note}}</div>

Or do I have to by hand add these extra tags from the documentation (Working with forms | Django documentation | Django) for each input.

{{ form.non_field_errors }}
<div class="fieldWrapper">
    {{ form.subject.errors }}
    <label for="{{ form.subject.id_for_label }}">Email subject:</label>
    {{ form.subject }}
<div class="fieldWrapper">
    {{ form.message.errors }}
    <label for="{{ form.message.id_for_label }}">Your message:</label>
    {{ form.message }}

This looks a bit better, but still… feels like it could be a bit DRYer

<div class="fieldWrapper">
    {{ form.subject.errors }}
    {{ form.subject.label_tag }}
    {{ form.subject }}

Which just circles around to the earlier point of you taking advantage of the libraries available to you for managing this - templates and html fragments, Crispy forms and layouts, and bootstrap (or some other CSS framework supported by Crispy).

There really is a dichotomy between “flexible and general” and “straight-forward and direct”. Take your pick.

My “issue” with crispy is that I am rather new to django and I have not used Bootstrap a lot. Using 2 big frameworks that I do not know at the same time with a lot of connection between them, makes it easier, in my experience, to get lost. I like to use django libraries but I want to avoid to add a large CSS and JS frameworks.

If anyone finds this and is interested, I found a way to do this in django 4.1. It is not supported with all browsers though (firefox does not support it yet).

In the example below, I use a gird with 8 columns. “Normal fields” span 4 Colums, wide 8 (and for zip codes 1 and city 3.)

In the form with the widget, add a css class:

widgets = { 'comments': forms.Textarea(attrs={'class': 'grid-wide'})}

in the html:

<div class="grid">

in the css:

  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 1rem;

.grid> * {
  grid-column: span 4;

.grid > div:has(.grid-wide){
  grid-column: span 8;

The :has() selector is which is basically a parent selector is not supported in Firefox.

Ok, so to get back to your original question, you wrote:

If you wanted to render a form that way using a Crispy helper, the layout object for this form might look something like this:

  Row('first_name', 'last_name),
  Row('zip_code', 'city'),

No need for you to know or (initially) understand the css that makes this happen. Yes it uses bootstrap (among others), but it doesn’t require you to know it.