Django: how to override textarea globally with custom class?

I'm trying to override widgets (like textarea) globally in Django.

For this, I defined my own django/form/widgets/textarea.html file.

First, I wanted to modify the default number of rows. The textarea.html file will looked like this:

<textarea
  rows="3"
  name="{{ widget.name }}"
  {% include "django/forms/widgets/attrs.html" %}
>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

This example above works perfectly.

But what if I want to add a custom class now?

<textarea
  rows="3"
  class="my-custom-class"
  name="{{ widget.name }}"
  {% include "django/forms/widgets/attrs.html" %}
>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

This won't work as expected because the attrs.html is supposed to handle extra attributes like class. Hence, defining a class attribute directly in textarea would break this behaviour and would erase existing classes.

What is the "clean" way to add extra classes globally?

I'd like to do it in HTML, not in Python. For example, I don't want to define a new Widget which would inherit from the base widget when I'm defining my forms, as I think this is pure markup topic, and should not interfere with form definition itself.

Thanks.

With the help of this question, I found a solution. Feel free to improve it if you think you have a better one.

TL/DR: we're building a custom filter to detect the type of widget, map classes to it, and then use django-widget-tweaks to inject the class to the field, without altering the existing ones.

Now I can customise all my widget classes from a single place, without Python inheritance, and without overriding Django widget html files.

Template tag

from django import template

register = template.Library()

BASIC_INPUT = "border border-indigo-300 px-2.5 py-1.5 rounded-md focus:outline-none"

mapping = {
    "Select": BASIC_INPUT,
    "TextInput": BASIC_INPUT,
    "EmailInput": BASIC_INPUT,
    "RegionalPhoneNumberWidget": BASIC_INPUT,
    "ModelSelect2": "",  # let the default markup
    # add all widgets you could use, or use a default one and override the others.

}


@register.filter("get_field_classes")
def get_field_classes(field):
    widget_class_name = field.field.widget.__class__.__name__
    try:
        return mapping[widget_class_name]
    except KeyError:
        # you could use default classes rather than raising an error if you prefer
        raise ValueError(f"Classes related to {widget_class_name} are not defined yet")

Then in your default form template (e.g: django/forms/default.html), when browsing fields, with the help of django-widget-tweaks package:

{% load widget_tweaks %}
{% load get_field_classes %}

{% for field in form.visible_fields %}

  {# .... #}

  <div>
    {% with field_classes=field|get_field_classes %}
        {{ field|add_class:field_classes }}
    {% endwith %}
  </div>
{% endfor %}
Back to Top