In Django 5.0+, what does BoundField.as_field_group() render, and how do you restyle every form field on a site with one template?
answer
- label, help text, errors, widget
- a template per field group
- a renderer attribute, set project-wide
- per field or per call too
- overrides need the right renderer
basics
~10 sas_field_group() renders one field's label, help text, errors and widget through a field template, django/forms/field.html by default. To restyle every field, point a custom FORM_RENDERER's field_template_name at your own template.
solid answer
~40 sSince Django 5.0 a **field group** is a field's label, help text, errors and widget rendered together by `{{ form.amount.as_field_group }}`, using `BoundField.template_name`: the field's own `template_name` if set, otherwise the renderer's `field_template_name`, `django/forms/field.html` by default. The default div form template calls `as_field_group` for every field, so one template controls every field on the site. Subclass a renderer — typically `TemplatesSetting`, so the template is found through your `TEMPLATES` setting — set `field_template_name = "donations/forms/field.html"`, and point `FORM_RENDERER` at it. For a single field pass `template_name=` to the form field; for one render call use `form["amount"].render("...")`. The template gets one variable, `field`, the `BoundField`.
code
python · 14 linesfrom django import forms
class DonationForm(forms.Form):
amount = forms.DecimalField(
min_value=1,
decimal_places=2,
template_name="donations/forms/amount_field.html",
)
email = forms.EmailField()
form = DonationForm()
compact_email = form["email"].render("donations/forms/compact_field.html")go deeper
Recall that as_field_group renders a field's label, help text, errors and widget together and that the default template is django/forms/field.html.
Explain the lookup order — field template_name, then the renderer's field_template_name — and how a TemplatesSetting subclass in FORM_RENDERER applies one template site-wide.
Roll a site-wide field template out safely: keep {{ field }} and the help-text and error ids, and check admin and third-party forms the renderer also affects.
Choose between shared templates, a custom BoundField and a component library as the product's form contract, weighing how design changes and accessibility fixes propagate.
## What a field group is Before Django 5.0, a page that wanted its own field markup repeated the same block for every field: `label_tag`, a help-text `<div>`, `errors`, then the widget. Django 5.0 named that bundle a **field group** and gave it a template. `BoundField.as_field_group()` renders the field through its `template_name`, and the default template, `django/forms/field.html`, outputs: 1. The label — `{{ field.label_tag }}`, or, for widgets with `use_fieldset = True` such as `RadioSelect`, `CheckboxSelectMultiple`, `MultiWidget` and `SelectDateWidget`, a `<fieldset>` opened with `{{ field.legend_tag }}`. 2. The help text in `<div class="helptext" id="<auto_id>_helptext">`. 3. `{{ field.errors }}`. 4. The widget, `{{ field }}`, and the closing `</fieldset>` where one was opened. The field template's context holds a single variable, `field` — the `BoundField` — so everything else is reached through its attributes. ## Where the template name comes from `BoundField.template_name` is `field.template_name or renderer.field_template_name`. That gives three levels of control: | scope | how | example | |---|---|---| | whole project | `field_template_name` on the `FORM_RENDERER` class | every field on the donation site | | one field | `template_name=` on the form field | a larger amount picker | | one render | `BoundField.render(template_name)` | a compact field in a sidebar | The default form template, `django/forms/div.html`, renders each visible field with `{{ field.as_field_group }}`, so the project-wide template applies to every `{{ form }}` and `form.as_div()` without touching page templates. `as_p()`, `as_ul()` and `as_table()` use their own templates and do not call `as_field_group`, so they ignore it. ## Restyling a whole site The scenario: a donation site wants every field — amount, frequency, email, gift message — to share one look with its own classes and hint markup. ```python # donations/renderers.py from django.forms.renderers import TemplatesSetting class DonationFormRenderer(TemplatesSetting): field_template_name = "donations/forms/field.html" ``` ```python # settings.py FORM_RENDERER = "donations.renderers.DonationFormRenderer" ``` ```django {# donations/templates/donations/forms/field.html #} <div class="donation-field{% if field.errors %} has-error{% endif %}"> {% 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 %} {{ field }} {% if field.help_text %}<p class="hint"{% if field.auto_id %} id="{{ field.auto_id }}_helptext"{% endif %}>{{ field.help_text }}</p>{% endif %} {{ field.errors }} {% if field.use_fieldset %}</fieldset>{% endif %} </div> ``` Points worth knowing: - **Why `TemplatesSetting`.** The default renderer, `DjangoTemplates`, runs its own template engine that searches Django's built-in form templates first and then apps' `templates/` directories; it ignores the project's `TEMPLATES` setting. A brand-new template name inside an app's `templates/` directory would still be found, but a template in the project's `DIRS`, or one that replaces a built-in name such as `django/forms/field.html`, needs `TemplatesSetting`, which finds templates the way the rest of the project does. It needs the built-in form templates to be findable too — most simply `"django.forms"` in `INSTALLED_APPS` with an engine using `APP_DIRS=True`. - **Keep the widget as `{{ field }}`.** Rendering the widget through the `BoundField` keeps its `id`, `name`, value, `required`, `aria-invalid` and `aria-describedby` attributes; the template changes layout, not the input. - **Keep the ids.** `aria-describedby` points at `<auto_id>_helptext` and, since 5.2, at the error list's `<auto_id>_error`; a template that drops those ids breaks the links. - **It is project-wide.** `FORM_RENDERER` also applies to the admin and third-party forms that use the default renderer, so test those pages after the switch. ## Rolling it out A site-wide field template is a change to every form at once, so treat it like one: 1. Render one form of each kind — text inputs, a `RadioSelect`, a `CheckboxInput`, a `SelectDateWidget`, a field with errors — and compare the HTML before and after. 2. Check pages that render fields by hand; they do not use the template and may now look inconsistent. 3. Visit the admin and any third-party forms, which use the same renderer. 4. Keep per-page layout in page templates with `as_field_group`, and per-form layout in `Form.template_name`, so the field template stays about one field. ## When a template is not enough Since Django 5.2 you can also swap the `BoundField` class itself — `bound_field_class` on the renderer, on a `Form`, or on a single field — to change behaviour such as `css_classes()` in Python. A template changes markup; a custom `BoundField` changes what the template receives. Before 5.2 the only hook was overriding `Field.get_bound_field()`.
- What does bound_field_class add in Django 5.2 that a field template cannot do?It swaps the `BoundField` class, so you can change Python behaviour such as `css_classes()` or add properties the template reads. It can be set on the renderer for the project, on a `Form` class, or on a single field, with the field taking precedence. Earlier releases required overriding `Field.get_bound_field()`.
- Why does a project-wide field template not change the output of form.as_p()?Only the div form template renders fields through `as_field_group()`. `as_p()`, `as_ul()` and `as_table()` use templates that write label, widget and help text directly, so they never read `field_template_name`.
saying these in an interview costs you the question
- as_field_group renders only the widget, without label or errors
- the default DjangoTemplates renderer picks up field.html from the project's DIRS
- a field template must write the <input> element by hand
- setting field_template_name changes the output of as_table() too
- a custom FORM_RENDERER leaves admin forms unaffected