Skip to main content

Django Form Layouts

When building web applications with Django, forms are a critical component of your user interface. While Django provides powerful form validation and processing capabilities, the default form rendering might not always meet your design requirements. Understanding how to customize form layouts is essential for creating professional, user-friendly applications.

Introduction to Django Form Layouts

Django forms can be rendered in various ways to match your application's design needs. By default, Django provides several rendering methods, but you can also create completely custom layouts. In this tutorial, we'll explore different approaches to form layout, from simple built-in methods to advanced custom templates.

Basic Form Rendering Methods

Django forms come with several built-in rendering methods that let you output the entire form or individual fields in different ways.

The Default Rendering

When you simply output a form in a template, Django renders it as an HTML table:

python
# forms.py
from django import forms

class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
html
<!-- template.html -->
<form method="post">
{% csrf_token %}
{{ form }}
<button type="submit">Submit</button>
</form>

The output will be:

html
<tr>
<th><label for="id_name">Name:</label></th>
<td><input type="text" name="name" maxlength="100" required id="id_name"></td>
</tr>
<tr>
<th><label for="id_email">Email:</label></th>
<td><input type="email" name="email" required id="id_email"></td>
</tr>
<tr>
<th><label for="id_message">Message:</label></th>
<td><textarea name="message" cols="40" rows="10" required id="id_message"></textarea></td>
</tr>

Alternative Rendering Methods

Django provides these built-in methods for different layouts:

  1. {{ form.as_table }} - Renders fields as table rows (default)
  2. {{ form.as_p }} - Renders fields wrapped in <p> tags
  3. {{ form.as_ul }} - Renders fields as list items

Let's see the differences:

html
<!-- Using as_p -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit</button>
</form>

<!-- Using as_ul -->
<form method="post">
{% csrf_token %}
<ul>
{{ form.as_ul }}
</ul>
<button type="submit">Submit</button>
</form>

The as_p version will output:

html
<p>
<label for="id_name">Name:</label>
<input type="text" name="name" maxlength="100" required id="id_name">
</p>
<p>
<label for="id_email">Email:</label>
<input type="email" name="email" required id="id_email">
</p>
<p>
<label for="id_message">Message:</label>
<textarea name="message" cols="40" rows="10" required id="id_message"></textarea>
</p>

Field-by-Field Rendering

For more control, you can render each form field individually:

html
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.name.id_for_label }}">Name:</label>
{{ form.name }}
{% if form.name.errors %}
<div class="errors">{{ form.name.errors }}</div>
{% endif %}
</div>

<div class="form-group">
<label for="{{ form.email.id_for_label }}">Email:</label>
{{ form.email }}
{% if form.email.errors %}
<div class="errors">{{ form.email.errors }}</div>
{% endif %}
</div>

<div class="form-group">
<label for="{{ form.message.id_for_label }}">Your Message:</label>
{{ form.message }}
{% if form.message.errors %}
<div class="errors">{{ form.message.errors }}</div>
{% endif %}
</div>

<button type="submit">Submit</button>
</form>

This gives you complete control over the placement and styling of each form element.

Using CSS Frameworks with Django Forms

Most modern websites use CSS frameworks like Bootstrap or Tailwind CSS. Here's how to integrate Bootstrap with Django forms:

Adding Bootstrap Classes to Form Fields

You can add CSS classes to form fields in your form definition:

python
# forms.py
from django import forms

class ContactForm(forms.Form):
name = forms.CharField(
max_length=100,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
message = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5
})
)

Then in your template:

html
<form method="post" class="container mt-4">
{% csrf_token %}

<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">Name</label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
{% endif %}
</div>

<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">Email</label>
{{ form.email }}
{% if form.email.errors %}
<div class="invalid-feedback d-block">{{ form.email.errors }}</div>
{% endif %}
</div>

<div class="mb-3">
<label for="{{ form.message.id_for_label }}" class="form-label">Message</label>
{{ form.message }}
{% if form.message.errors %}
<div class="invalid-feedback d-block">{{ form.message.errors }}</div>
{% endif %}
</div>

<button type="submit" class="btn btn-primary">Submit</button>
</form>

Form Layout with Django Crispy Forms

For more advanced form layouts, django-crispy-forms is an excellent package. It allows you to define your form layout in Python code rather than in templates.

Installing Crispy Forms

First, install the package:

bash
pip install django-crispy-forms

Add it to your INSTALLED_APPS in settings.py:

python
INSTALLED_APPS = [
# ... other apps
'crispy_forms',
]

# Set the default template pack
CRISPY_TEMPLATE_PACK = 'bootstrap4' # or 'bootstrap5' if using Bootstrap 5

Using Crispy Forms Layout

Now you can define advanced layouts in your forms:

python
# forms.py
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div

class AdvancedContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
phone = forms.CharField(max_length=15, required=False)
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Row(
Column('name', css_class='form-group col-md-6'),
Column('email', css_class='form-group col-md-6'),
css_class='form-row'
),
Row(
Column('phone', css_class='form-group col-md-6'),
Column('subject', css_class='form-group col-md-6'),
css_class='form-row'
),
'message',
Submit('submit', 'Send Message', css_class='btn btn-primary')
)

In your template, simply include:

html
{% load crispy_forms_tags %}

<div class="container">
{% crispy form %}
</div>

This will render a professionally styled form with the specified layout structure without writing extensive HTML in your template.

Custom Form Templates

For complete control, you can create custom form templates. This approach is useful for complex forms or when you need a consistent look across multiple forms.

Creating a Form Snippet Template

Create a template file like _form_field.html:

html
<!-- templates/_form_field.html -->
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}{% if field.field.required %}<span class="required">*</span>{% endif %}
</label>

{{ field }}

{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}

{% if field.errors %}
<div class="error-message">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

Then use the template for each field:

html
<form method="post">
{% csrf_token %}

{% for field in form %}
{% include "_form_field.html" %}
{% endfor %}

<button type="submit" class="btn btn-primary">Submit</button>
</form>

Real-World Example: Multi-Step Form

Let's implement a multi-step form with progress indicators for a user registration process:

python
# forms.py
from django import forms

class UserDetailsForm(forms.Form):
first_name = forms.CharField(max_length=50)
last_name = forms.CharField(max_length=50)
email = forms.EmailField()

class UserAddressForm(forms.Form):
address = forms.CharField(max_length=200)
city = forms.CharField(max_length=100)
postal_code = forms.CharField(max_length=10)
country = forms.CharField(max_length=100)

class UserPreferencesForm(forms.Form):
newsletter = forms.BooleanField(required=False)
preferences = forms.MultipleChoiceField(
choices=(
('tech', 'Technology'),
('sports', 'Sports'),
('entertainment', 'Entertainment'),
('science', 'Science'),
),
widget=forms.CheckboxSelectMultiple,
required=False
)

In your views:

python
# views.py
from django.shortcuts import render, redirect
from .forms import UserDetailsForm, UserAddressForm, UserPreferencesForm

def registration_step1(request):
if request.method == 'POST':
form = UserDetailsForm(request.POST)
if form.is_valid():
# Store data in session
request.session['registration_step1'] = form.cleaned_data
return redirect('registration_step2')
else:
form = UserDetailsForm()

return render(request, 'registration/step1.html', {
'form': form,
'current_step': 1,
'total_steps': 3
})

# Similar views for step2 and step3

In your template:

html
<!-- templates/registration/step1.html -->
<div class="container">
<div class="progress mb-4">
<div class="progress-bar" role="progressbar" style="width: {{ current_step|divisibleby:total_steps|multiply:100 }}%">
Step {{ current_step }} of {{ total_steps }}
</div>
</div>

<h2>Step {{ current_step }}: Personal Details</h2>

<form method="post">
{% csrf_token %}

{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<div class="text-danger">
{{ field.errors }}
</div>
{% endif %}
</div>
{% endfor %}

<div class="form-navigation">
<button type="submit" class="btn btn-primary">Next</button>
</div>
</form>
</div>

Horizontal Form Layouts

For a more compact form with labels beside the fields (instead of above):

html
<form method="post">
{% csrf_token %}

{% for field in form %}
<div class="row mb-3">
<label for="{{ field.id_for_label }}" class="col-sm-2 col-form-label">
{{ field.label }}
</label>
<div class="col-sm-10">
{{ field }}
{% if field.errors %}
<div class="text-danger">{{ field.errors }}</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
</div>
{% endfor %}

<div class="row">
<div class="col-sm-10 offset-sm-2">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>

Summary

Django offers multiple ways to control form layout and presentation:

  1. Built-in rendering methods (as_table, as_p, as_ul) provide basic formatting options
  2. Field-by-field rendering gives you complete control over the placement of each field
  3. CSS framework integration allows you to apply classes to form fields for better styling
  4. django-crispy-forms provides an elegant Python API to define complex form layouts
  5. Custom form templates give you the most flexibility for consistent, reusable form designs

By mastering these techniques, you can create user-friendly, professionally styled forms that match your application's design while still leveraging Django's form validation capabilities.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Form Styling: Create a contact form using Django's built-in methods and style it with CSS to match a professional design.

  2. Field-by-Field Rendering: Convert a form that uses {{ form.as_p }} to use field-by-field rendering with custom error messages and help text.

  3. Bootstrap Integration: Take an existing Django form and integrate it with Bootstrap, including validation styling.

  4. Crispy Forms Layout: Install django-crispy-forms and implement a complex form layout with multiple sections and columns.

  5. Multi-Step Form: Create a multi-step user registration form that saves data between steps and displays a progress indicator.

By practicing these exercises, you'll develop the skills to create forms that are not only functional but also provide a great user experience.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)