# django-widget-tweaks: let the template own the CSS classes instead of your form classes

> A JazzBand package that adds one template tag and a dozen filters, so a designer can restyle a Django field without anyone editing a Python form definition.

**jazzband/django-widget-tweaks** — Tweak the form field rendering in templates, not in python-level form definitions. CSS classes and HTML attributes can be altered. 

- Repository: https://github.com/jazzband/django-widget-tweaks
- Stars: 2,164 · Forks: 138
- Language: Python
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/jazzband-django-widget-tweaks

## The problem is that Django's form rendering has exactly three modes

The README states the motivation in one sentence: tweak the form field rendering in templates, not in python-level form definitions. Altering CSS classes and HTML attributes is supported, and that should be enough for designers to customize field presentation using CSS and unobtrusive javascript without touching python code.

The friction it removes is real. Django ships `as_p`, `as_div` and `as_table`, so if you want a fourth layout or a specific class on one input, the usual options are to write your own template fragment, subclass the widget, or pass an `attrs` dict from Python. The last of those puts presentation decisions in the layer that is supposed to hold business logic, which is exactly the coupling the package exists to avoid.

Installation is the usual two steps for a JazzBand app: install from PyPI, then add the app name to `INSTALLED_APPS`.

```python
INSTALLED_APPS += [
    'widget_tweaks',
]
```

The version story lives in `setup.py`, which asks for Python 3.9 or newer and Django 4.2 or newer, with classifiers covering Django 4.2 through 5.2 and Python 3.9 through 3.13. It is authored by Mikhail Korobov and MIT licensed.

## Two tools, and the README is clear about which one you should reach for

The package provides two sets of tools that can be used together or on their own. There is a `render_field` template tag for customizing form fields with an HTML-like syntax, and there are several template filters for customizing field HTML attributes and CSS classes.

The README's verdict is worth quoting because it saves you an afternoon. The tag should be easier to use and should make customizations much easier for designers and front-end developers. The filters are more powerful than the tag, but they use a more complex and less HTML-like syntax.

That is the correct prioritization. `render_field` reads like markup, so a designer working in a template can add a class without learning a filter mini-language. The filters earn their keep when you need logic, and most of them exist to be conditional in ways a plain attribute is not.

The tag syntax itself is where the ergonomics are. You pass the field and then attribute-like arguments, and the tag understands a few conventions beyond plain attributes. Appending to an existing value uses `class+=`, so a field's own class survives:

```html+django
    <!-- append to an attribute -->
    {% render_field form.title class+="css_class_1 css_class_2" %}
```

Template variables work as attribute values, which is how you keep the placeholder in sync with the label instead of hardcoding it twice. And a double colon passes a namespaced attribute straight through, which is what makes the tag usable with Vue-style bindings rather than forcing a JavaScript workaround.

```html+django
    <!-- double colon -->
    {% render_field form.search_query v-bind::class="{active:isActive}" %}
```

## Error and required classes set as context variables, not per field

One feature is specific to the tag rather than the filters. For fields rendered with `render_field`, you can set an error class and a required-fields class using the `WIDGET_ERROR_CLASS` and `WIDGET_REQUIRED_CLASS` template variables.

```html+django
    {% with WIDGET_ERROR_CLASS='my_error' WIDGET_REQUIRED_CLASS='my_required' %}
        {% render_field form.field1 %}
        {% render_field form.field2 %}
        {% render_field form.field3 %}
    {% endwith %}
```

Wrapping the fields in a single `{% with %}` block is the point. Setting them once for a group means you are not repeating the same class name on twenty fields, and a context processor could set a default error class for every field the tag renders, which the README suggests as the creative use.

That is a different shape from the filters, where conditionality lives inside each filter invocation. Here the condition is expressed once in the template context and applies to a scope. If you are rendering a long form, that is the difference between one line of setup and a find-and-replace across the template.

## The filter set, and the two that are not just syntax sugar

The filters divide cleanly into two groups, and knowing which is which tells you where the real functionality is.

Most are thin wrappers over `attr`, which adds or replaces any single HTML attribute. `add_class` adds CSS classes, split on whitespace so you can add several at once. `set_data` sets an HTML5 data attribute and is described as a shortcut for `attr` that prepends `data-` to the name, with the example `{{ form.title|set_data:"filters:OverText" }}` producing a data attribute that unobtrusive JavaScript can read. `append_attr` appends a value to an existing attribute rather than replacing it, and `add_class` is itself a shortcut for `append_attr` on the `class` attribute. `remove_attr` deletes an attribute outright. And `add_label_class` is `add_class` pointed at the label element rather than the field.

The other two are not sugar, because they inspect the field. `add_error_class` applies a class only when validation has failed, meaning `field.errors` is not empty. `add_error_attr` does the same for an arbitrary attribute, and the README calls out the accessibility use case specifically, with `{{ form.title|add_error_attr:"aria-invalid:true" }}`.

That last one is the filter to remember. Marking an invalid field for screen readers is exactly the kind of thing that gets skipped when you are hand-rolling form templates, and here it is a four-word filter. `add_required_class` completes the set by keying off the required flag rather than validation state.

There are also two introspection filters, `field_type` and `widget_type`, which return the field class name and the widget class name in lower case, so you can write a wrapper div whose classes depend on what Django actually instantiated.

## A two-year gap between minor releases, and a BoundField rework

The release history explains a lot about how JazzBand works as a collective. Version 1.5.0 shipped on 2023-08-25 with an empty changelog, and 1.4.12 came before it on 2022-01-13, also empty. Then 1.5.1 arrived on 2025-04-25 with a substantial list: Django 5.0 support from adamchainz, a `setup.py` update, documentation on rendering form errors, a dependabot config, a fix for a `re.split` deprecation warning, and a test pass that dropped end-of-life Python and Django versions.

Two entries in that list matter more than the rest. The `re.split` deprecation fix is the kind of thing that only surfaces on a newer Python, and it is a reminder that a small template package can still be bitten by a standard library change. And creating support for BoundWidget, built on issue 122 and accompanied by tests, is an API-level change for anyone who was reaching into the widget object directly.

The drop of end-of-life versions explains the current floors in `setup.py`. A Python 3.9 minimum and Django 4.2 minimum are the residue of that cleanup, not an arbitrary choice, and the classifier list running to Django 5.2 and Python 3.13 reflects what was current when the file was last updated.

The repository is MIT licensed with 2,163 stars, 138 forks and 49 open issues. There is no homepage in the repository metadata, and the tree is small: a `widget_tweaks/` package with a templatetags subpackage, `tests/`, `tox.ini`, `.coveragerc`, a changelog in `CHANGES.rst` and the usual JazzBand conduct and contributing files. The last push was on 2026-08-24.

## Conclusion

The reason this package still earns a dependency slot is narrow and durable. Django's `as_p`, `as_div` and `as_table` give you three fixed presentations, and everything past that normally means writing a template fragment per field or subclassing the widget in Python. This gives you the fourth option without either. Reach for `render_field` when you want HTML-like syntax in templates and the filters when you need conditional behaviour; the accessibility-oriented `add_error_attr` is the one worth knowing about, since it does something plain `attr` cannot.

## FAQ

### What does django-widget-tweaks replace?

It replaces the need to subclass a widget in Python or hand-write a template fragment per field just to add a CSS class or an HTML attribute. Django's built-in form rendering gives you three fixed layouts through `as_p`, `as_div` and `as_table`, and this package adds the next layer of control from the template instead of from the form definition.

### Should I use render_field or the template filters?

Use `render_field` when you can. It takes HTML-like arguments, so `{% render_field form.title class+="extra" %}` reads like markup and a designer can work with it. The filters are more powerful but use a colon-separated syntax. The README recommends the tag for ease of use and reserves the filters for cases where you need the extra logic.

### How do I mark a field as invalid for accessibility?

Use the `add_error_attr` filter, which sets an attribute only when validation failed for the field. The README gives `{{ form.title|add_error_attr:"aria-invalid:true" }}` as the accessibility example, producing `aria-invalid="true"` on fields with errors. `add_error_class` does the same thing for CSS classes.

### How do I append a class without losing the existing one?

In the tag, use the `+=` form, for example `{% render_field form.title class+="css_class_1 css_class_2" %}`. With the filters, use `append_attr` on the class attribute, which appends rather than replaces; `add_class` is documented as a shortcut for exactly that. Plain `attr` and `add_class` semantics differ here, so check which you are using.

### Which Django versions does django-widget-tweaks support?

Django 4.2 and later, on Python 3.9 or newer. `setup.py` sets `python_requires=">=3.9"` and `install_requires` asks for django 4.2 or newer, with classifiers covering Django 4.2 through 5.2 and Python 3.9 through 3.13. Those floors arrived in version 1.5.1, whose changelog records dropping end-of-life versions.

## Sources

- [Issues](https://github.com/jazzband/django-widget-tweaks/issues)
- [jazzband/django-widget-tweaks on GitHub](https://github.com/jazzband/django-widget-tweaks)
- [License: MIT](https://github.com/jazzband/django-widget-tweaks/blob/master/LICENSE)
- [README](https://github.com/jazzband/django-widget-tweaks/blob/master/README.md)
- [Releases](https://github.com/jazzband/django-widget-tweaks/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jazzband-django-widget-tweaks
