Form templates#
The {% form "name" %} block tag renders a <form> element, injects the CSRF token, and publishes the form instance inside the block body.
The form tag#
{% form "article_edit_form" %}
{{ form.title }}
{{ form.body }}
<button type="submit">Save</button>
{% endform %}
The first argument is the action name as a quoted string or a context variable that resolves to a string.
An opening tag without the action name raises TemplateSyntaxError at parse time.
Optional key="value" arguments after the name render as HTML attributes on the <form> element (see HTML attributes below), except for the reserved partial param names (see Partial attributes below).
The tag does the following.
Looks up the action name in the registry, preferring a page-scoped match for the current page, then falling back to shared scope.
Resolves the stable dispatch URL for that action.
Emits
<form action="..." method="post" data-next-action="...">plus an automaticenctype="multipart/form-data"for a multipart form, then any attributes passed to the tag.Emits a hidden
csrfmiddlewaretokeninput.Emits a hidden
_next_form_origininput set torequest.path, used byredirect_to_originon success and resolved through the URLconf on the error re-render.Publishes
forminside the block body (see The form Variable below).
On the validation-error re-render the request targets the dispatch endpoint, so the tag re-emits the posted origin of the original page instead of request.path.
On a wizard advance in a partial render the shaping layer sets the FORM_ORIGIN_OVERRIDE_KEY context key to the next step URL, and that value wins over the posted origin.
The data-next-action attribute carries the action UID, the registry identity that also names the dispatch URL.
The tag emits it when the action meta is available, which makes the form addressable from client-side scripts without parsing the action URL.
A backend whose meta stores no uid renders the form without the attribute.
A multipart form gets enctype="multipart/form-data" automatically.
The tag asks the form instance through is_multipart(), so a FileField rendered with a stock widget needs no extra argument.
A name that is not in the registry raises FormActionNotFoundError at render time.
HTML attributes#
Every other key="value" argument after the action name lands on the <form> element, after the framework attributes.
{% form "attachment_form" class="stack" id="upload" %}
{{ form.file }}
<button type="submit">Upload</button>
{% endform %}
Attribute values are escaped, and an unquoted value resolves as a context variable.
An explicit enctype="..." argument suppresses the automatic multipart value, for the rare form that needs a different encoding.
The action and method attributes belong to the tag, as does every attribute starting with data-next-, and passing any of them raises TemplateSyntaxError at parse time.
data-next-* is the single framework namespace in rendered markup, so user attributes never collide with framework ones.
Partial attributes#
Five argument names are reserved for partial rendering and never land as raw HTML attributes.
The tag compiles each one to a data-next-* attribute the client runtime reads.
Tag argument |
Rendered attribute |
Carries |
|---|---|---|
|
|
The client-side validation mode for the form. |
|
|
The event that triggers a partial submission. |
|
|
The debounce interval before a triggered submission fires. |
|
|
The zone the partial response replaces. |
|
|
The match key that names the form instance among repeated forms, so a partial morph lands on the submitted copy. |
See Partial rendering reference for the attribute semantics and Partial rendering for the topic.
Pass each as a key="value" argument like any other, and the value resolves as a string literal or a context variable the same way an HTML attribute value does.
{% form "filter_form" trigger="change" debounce="200" zone="results" %}
{{ form.query }}
{% endform %}
Scope resolution#
When the template renders inside a page, the tag first looks for a page-scoped registration whose file matches the current page.py.
If no page-scoped match exists the tag falls back to the first registration of that name and accepts it only when its scope is shared.
This means a page-local NoteForm takes precedence over a shared NoteForm with the same derived name.
The form variable#
Inside the block body the variable form holds the form instance.
- Initial render (GET).
The framework calls
get_initialthrough the dependency resolver, constructs an unbound form from the returned data or instance, and publishes it asform.- Re-rendered page after a failing POST.
The variable is the bound form with validation errors attached. The template renders the user input plus any error messages without branching.
- Form-less action.
When the action is a plain function registered with
@action(no form class),formresolves toNone. The block body should not attempt to render field widgets in this case.
Captured URL parameters#
The tag does not need any extra argument to forward URL parameters.
The captured kwargs travel inside the origin path itself.
The dispatcher resolves the posted _next_form_origin against the URLconf and recovers every kwarg through the real URL converters, skipping names reserved by the dependency resolver.
{% form "note_form" %}
{{ form.title }}
<button type="submit">Save</button>
{% endform %}
A form rendered under /notes/42/ posts _next_form_origin set to that path, and resolving it yields note_id=42 as an int.
The handler receives the value through DUrl["note_id", int], typed identically on the canonical GET and on the re-render.
Multiple forms on one page#
Each {% form %} call references a different action name.
The dispatcher routes submissions by URL alone, so the forms do not interfere.
{% form "note_form" %}
{{ form.title }}
{{ form.body }}
<button type="submit">Save</button>
{% endform %}
{% form "delete_note" %}
<button type="submit" class="danger">Delete</button>
{% endform %}
delete_note is a form-less action.
The second block has no {{ form }} usage because form is None.
Rendering field errors#
Errors live on the bound form. Render them inline or as a summary at the top.
{% form "note_form" %}
<div>
{{ form.title }}
{% if form.title.errors %}
<p class="error">{{ form.title.errors|first }}</p>
{% endif %}
</div>
<button type="submit">Save</button>
{% endform %}
{% form "note_form" %}
{% if form.errors %}
<ul>
{% for field, messages in form.errors.items %}
{% for message in messages %}<li>{{ field }} — {{ message }}</li>{% endfor %}
{% endfor %}
</ul>
{% endif %}
{{ form.title }}
{{ form.body }}
<button type="submit">Save</button>
{% endform %}
Manual CSRF#
The tag emits csrfmiddlewaretoken automatically.
Only add Django’s {% csrf_token %} manually when you build the <form> element by hand and skip the tag entirely.
A hand-crafted form must also include the _next_form_origin hidden field or the dispatcher cannot re-render on failure.
Set it to the URL path of the page, the same value the tag emits, with {{ request.path }} as the natural source.
The {% action_url %} tag resolves the dispatch URL by action name with the same page scoping as {% form %}, so a hand-crafted form never hard-codes a UID.
It also supports as assignment for reuse, see Template tags.
<form action="{% action_url 'contact_form' %}" method="post">
{% csrf_token %}
<input type="hidden" name="_next_form_origin" value="{{ request.path }}">
<button type="submit">Send</button>
</form>
An unknown action name raises FormActionNotFoundError at render time, the same error the {% form %} tag raises.
Forms in hand-written views#
A {% form %} tag also works inside a template rendered by an ordinary Django view, outside the file router.
The success path needs nothing extra, the handler runs and its response goes out.
The error re-render is different.
The dispatcher resolves the posted origin to a view and reads the page source location from its next_page_path attribute, which the file router sets on every routed view and a hand-written view lacks.
Without it an invalid submission returns HTTP 400 instead of re-rendering.
Opt in by setting the attribute on the view function yourself, as a Path or a string naming the page.py location whose body the dispatcher should compose on the error re-render.
The file may be a real page module or a synthesised location next to a template.djx, exactly as for virtual routes.
from pathlib import Path
from django.shortcuts import render
def feedback(request):
return render(request, "feedback.html")
feedback.next_page_path = Path(__file__).parent / "feedback" / "page.py"
Common patterns#
Form in a component#
A component template hosts {% form %} exactly like a page template.
The framework injects current_page_module_path from the surrounding page, so the action lookup scopes to the correct page.
Form in a layout#
Layouts receive current_page_module_path from the page they wrap.
A login form placed in the root layout posts the wrapped page’s path as its origin, so a validation failure re-renders the original page.
Render-time failures#
The tag raises ImproperlyConfigured when request is absent from the template context.
Add django.template.context_processors.request to TEMPLATES[*].OPTIONS.context_processors to make the request available.
The next.E019 system check, described in CSRF and forms, catches the missing context processor before a request reaches the tag.
See also#
See also
Actions for auto-registration and the handler side of the contract. Validation and re-render for what runs after a failing submission. Template tags for every template tag the framework registers.