Django provides flexible options for rendering forms in templates, from automatic rendering to complete manual control. Understanding these rendering techniques enables you to create consistent, accessible, and visually appealing forms.
<!-- templates/basic_form.html -->
<form method="post">
{% csrf_token %}
<!-- Render entire form automatically -->
{{ form }}
<button type="submit">Submit</button>
</form>
<!-- Alternative rendering methods -->
<form method="post">
{% csrf_token %}
<!-- Render as paragraph -->
{{ form.as_p }}
<!-- Render as table -->
<table>
{{ form.as_table }}
</table>
<!-- Render as unordered list -->
<ul>
{{ form.as_ul }}
</ul>
<button type="submit">Submit</button>
</form>
<!-- templates/manual_form.html -->
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- Manual field rendering with full control -->
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }}
{% if form.name.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{{ form.name }}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Email field with custom styling -->
<div class="mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }}
{% if form.email.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
<div class="input-group">
<span class="input-group-text">@</span>
{{ form.email }}
</div>
{% if form.email.help_text %}
<div class="form-text">{{ form.email.help_text }}</div>
{% end
{{ form.newsletter.label_tag }}
<div class="form-check">
{{ form.newsletter }}
<label class="form-check-label" for="{{ form.newsletter.id_for_label }}">
Subscribe to newsletter
</label>
</div>
</div>
<div class="mb-3">
{{ form.language.label_tag }}
{{ form.language }}
{% if form.language.errors %}
<div class="text-danger">{{ form.language.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.timezone.label_tag }}
{{ form.timezone }}
{% if form.timezone.errors %}
<div class="text-danger">{{ form.timezone.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="mt-3 p-3 border-top">
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">Save Profile</button>
</div>
</div>
</form>
# templatetags/form_tags.py
from django import template
from django.forms.widgets import CheckboxInput, RadioSelect, CheckboxSelectMultiple
register = template.Library()
@register.inclusion_tag('forms/field.html')
def render_field(field, css_class='form-control'):
"""Custom field rendering template tag"""
return {
'field': field,
'css_class': css_class,
'is_checkbox': isinstance(field.field.widget, CheckboxInput),
'is_radio': isinstance(field.field.widget, RadioSelect),
'is_multiple_checkbox': isinstance(field.field.widget, CheckboxSelectMultiple),
}
@register.filter
def add_class(field, css_class):
"""Add CSS class to form field"""
return field.as_widget(attrs={'class': css_class})
@register.filter
def add_attrs(field, attrs):
"""Add multiple attributes to form field"""
attr_dict = {}
for attr in attrs.split(','):
key, value = attr.split(':')
attr_dict[key.strip()] = value.strip()
return field.as_widget(attrs=attr_dict)
@register.filter
def field_type(field):
"""Get field widget type"""
return field.field.widget.__class__.__name__
@register.simple_tag
def form_errors_count(form):
"""Count total form errors"""
count = len(form.non_field_errors())
for field in form:
count += len(field.errors)
return count
<!-- templates/forms/field.html -->
<div class="mb-3">
{% if not is_checkbox %}
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
{% endif %}
{% if is_checkbox %}
<div class="form-check">
{{ field }}
<label class="form-check-label" for="{{ field.id_for_label }}">
{{ field.label }}
{% if field.field.required %}
<span class="text-danger">*</span>
{% endif %}
</label>
</div>
{% elif is_radio %}
<div class="form-check-group">
{{ field }}
</div>
{% elif is_multiple_checkbox %}
<div class="form-check-group">
{{ field }}
</div>
{% else %}
{{ field }}
{% endif %}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- templates/custom_form.html -->
{% load form_tags %}
<form method="post">
{% csrf_token %}
<!-- Using custom field rendering -->
{% render_field form.name %}
{% render_field form.email css_class="form-control form-control-lg" %}
<!-- Using filters -->
{{ form.phone|add_class:"form-control" }}
{{ form.website|add_attrs:"class:form-control,placeholder:https://example.com" }}
<!-- Form error summary -->
{% form_errors_count form as error_count %}
{% if error_count > 0 %}
<div class="alert alert-danger">
<strong>{{ error_count }} error{{ error_count|pluralize }} found:</strong>
Please correct the highlighted fields.
</div>
{% endif %}
<button type="submit">Submit</button>
</form>
<!-- templates/accessible_form.html -->
<form method="post" role="form" aria-labelledby="form-title">
{% csrf_token %}
<h2 id="form-title">Contact Information Form</h2>
<!-- Required field indicator -->
<p class="form-required-info">
<span aria-hidden="true">*</span> indicates required fields
</p>
<!-- Fieldset grouping -->
<fieldset>
<legend>Personal Information</legend>
<div class="form-group">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }}
<span aria-label="required" class="required">*</span>
</label>
{{ form.name }}
{% if form.name.help_text %}
<div id="{{ form.name.id_for_label }}_help" class="form-text">
{{ form.name.help_text }}
</div>
{% endif %}
{% if form.name.errors %}
<div id="{{ form.name.id_for_label }}_error" class="invalid-feedback d-block"
role="alert" aria-live="polite">
{{ form.name.errors.0 }}
</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}" class="form-label">
{{ form.email.label }}
<span aria-label="required" class="required">*</span>
</label>
{{ form.email }}
{% if form.email.help_text %}
<div id="{{ form.email.id_for_label }}_help" class="form-text">
{{ form.email.help_text }}
</div>
{% endif %}
{% if form.email.errors %}
<div id="{{ form.email.id_for_label }}_error" class="invalid-feedback d-block"
role="alert" aria-live="polite">
{{ form.email.errors.0 }}
</div>
{% endif %}
</div>
</fieldset>
<fieldset>
<legend>Message</legend>
<div class="form-group">
<label for="{{ form.subject.id_for_label }}" class="form-label">
{{ form.subject.label }}
</label>
{{ form.subject }}
{% if form.subject.errors %}
<div id="{{ form.subject.id_for_label }}_error" class="invalid-feedback d-block"
role="alert" aria-live="polite">
{{ form.subject.errors.0 }}
</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.message.id_for_label }}" class="form-label">
{{ form.message.label }}
<span aria-label="required" class="required">*</span>
</label>
{{ form.message }}
<div id="{{ form.message.id_for_label }}_help" class="form-text">
Maximum 1000 characters. <span id="char-count">0/1000</span>
</div>
{% if form.message.errors %}
<div id="{{ form.message.id_for_label }}_error" class="invalid-feedback d-block"
role="alert" aria-live="polite">
{{ form.message.errors.0 }}
</div>
{% endif %}
</div>
</fieldset>
<!-- Form-wide errors -->
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert" aria-live="polite">
<h4>Form Errors</h4>
<ul>
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">
Send Message
</button>
<button type="reset" class="btn btn-secondary">
Clear Form
</button>
</div>
</form>
<script>
// Character counter for accessibility
document.addEventListener('DOMContentLoaded', function() {
const messageField = document.getElementById('{{ form.message.id_for_label }}');
const charCount = document.getElementById('char-count');
if (messageField && charCount) {
messageField.addEventListener('input', function() {
const count = this.value.length;
charCount.textContent = `${count}/1000`;
// Update ARIA attributes
if (count > 900) {
charCount.setAttribute('aria-live', 'assertive');
charCount.className = 'text-warning';
} else if (count > 950) {
charCount.className = 'text-danger';
} else {
charCount.setAttribute('aria-live', 'polite');
charCount.className = '';
}
});
}
});
</script>
<!-- templates/ajax_form.html -->
<div id="form-container">
<form id="ajax-form" method="post">
{% csrf_token %}
<div id="form-fields">
{% for field in form %}
<div class="form-group" data-field="{{ field.name }}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
<div class="field-errors"></div>
</div>
{% endfor %}
</div>
<div id="form-errors" class="alert alert-danger" style="display: none;"></div>
<button type="submit" id="submit-btn" class="btn btn-primary">
<span class="btn-text">Submit</span>
<span class="btn-spinner spinner-border spinner-border-sm" style="display: none;"></span>
</button>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('ajax-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = submitBtn.querySelector('.btn-text');
const btnSpinner = submitBtn.querySelector('.btn-spinner');
const formErrors = document.getElementById('form-errors');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Clear previous errors
clearErrors();
// Show loading state
setLoadingState(true);
// Submit form
const formData = new FormData(form);
fetch(form.action || window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Handle success
showSuccess(data.message || 'Form submitted successfully!');
form.reset();
} else {
// Handle errors
showErrors(data.errors, data.non_field_errors);
}
})
.catch(error => {
console.error('Error:', error);
showErrors({}, ['An error occurred. Please try again.']);
})
.finally(() => {
setLoadingState(false);
});
});
function clearErrors() {
// Clear field errors
document.querySelectorAll('.field-errors').forEach(el => {
el.innerHTML = '';
el.parentElement.classList.remove('has-error');
});
// Clear form errors
formErrors.style.display = 'none';
formErrors.innerHTML = '';
}
function showErrors(fieldErrors, nonFieldErrors) {
// Show field errors
for (const [fieldName, errors] of Object.entries(fieldErrors || {})) {
const fieldGroup = document.querySelector(`[data-field="${fieldName}"]`);
if (fieldGroup) {
const errorDiv = fieldGroup.querySelector('.field-errors');
errorDiv.innerHTML = errors.join('<br>');
fieldGroup.classList.add('has-error');
}
}
// Show non-field errors
if (nonFieldErrors && nonFieldErrors.length > 0) {
formErrors.innerHTML = '<ul>' +
nonFieldErrors.map(error => `<li>${error}</li>`).join('') +
'</ul>';
formErrors.style.display = 'block';
}
}
function showSuccess(message) {
const successDiv = document.createElement('div');
successDiv.className = 'alert alert-success';
successDiv.textContent = message;
form.parentNode.insertBefore(successDiv, form);
setTimeout(() => {
successDiv.remove();
}, 5000);
}
function setLoadingState(loading) {
submitBtn.disabled = loading;
if (loading) {
btnText.style.display = 'none';
btnSpinner.style.display = 'inline-block';
} else {
btnText.style.display = 'inline';
btnSpinner.style.display = 'none';
}
}
});
</script>
<!-- templates/bootstrap_form.html -->
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- Bootstrap 5 styling -->
<div class="row g-3">
{% for field in form %}
{% if field.field.widget.input_type == 'hidden' %}
{{ field }}
{% else %}
<div class="col-md-6">
<div class="form-floating">
{{ field }}
{{ field.label_tag }}
{% if field.errors %}
<div class="invalid-feedback">
{{ field.errors.0 }}
</div>
{% endif %}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger mt-3">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="mt-3">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
Django's template rendering system provides comprehensive tools for creating accessible, responsive, and user-friendly forms. By understanding automatic rendering, manual control, layout patterns, accessibility features, and AJAX integration, you can build sophisticated form interfaces that work across all devices and assistive technologies while maintaining clean, maintainable code.
Built-in Fields and Widgets
Django provides a comprehensive set of form fields and widgets that handle different data types and user interface elements. Understanding these built-in components enables you to create sophisticated forms without custom implementations.
Model Forms
Django's ModelForm class provides automatic form generation from model definitions, streamlining the process of creating forms that correspond to database models. This chapter covers ModelForm creation, customization, and advanced patterns for efficient data handling.