Widgets personalizados en formularios Django

Intermedio
Django
Django
Actualizado: 18/04/2026

Widgets en Django

Un widget en Django controla cómo se renderiza un campo del formulario como HTML. Cada tipo de campo tiene un widget por defecto (TextInput para CharField, CheckboxInput para BooleanField, etc.), pero se puede cambiar o configurar según las necesidades del diseño.

Diagrama conceptual de Widgets personalizados en formularios Django

Configurar widgets con attrs

La forma más sencilla de personalizar un widget es pasarle el diccionario attrs con atributos HTML adicionales:

from django import forms

class ProductoForm(forms.ModelForm):
    class Meta:
        model = Producto
        fields = ['nombre', 'precio', 'categoria', 'descripcion', 'fecha_lanzamiento']
        widgets = {
            'nombre': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Nombre del producto',
                'autofocus': True
            }),
            'precio': forms.NumberInput(attrs={
                'class': 'form-control',
                'min': '0',
                'step': '0.01'
            }),
            'descripcion': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'maxlength': 500
            }),
            'fecha_lanzamiento': forms.DateInput(attrs={
                'type': 'date',
                'class': 'form-control'
            }),
            'categoria': forms.Select(attrs={
                'class': 'form-select'
            }),
        }

Widgets para selección múltiple

class FiltroForm(forms.Form):
    categorias = forms.ModelMultipleChoiceField(
        queryset=Categoria.objects.all(),
        widget=forms.CheckboxSelectMultiple(attrs={'class': 'checkbox-list'}),
        required=False
    )
    etiquetas = forms.MultipleChoiceField(
        choices=ETIQUETAS_CHOICES,
        widget=forms.SelectMultiple(attrs={'class': 'form-select', 'size': '5'}),
        required=False
    )
    estado = forms.ChoiceField(
        choices=[('', 'Todos'), ('activo', 'Activo'), ('inactivo', 'Inactivo')],
        widget=forms.RadioSelect(attrs={'class': 'radio-group'}),
        required=False
    )

DateInput y TimeInput nativos

Para inputs de fecha y hora HTML5, específica el atributo type en el widget:

class EventoForm(forms.ModelForm):
    class Meta:
        model = Evento
        fields = ['titulo', 'fecha_inicio', 'hora_inicio', 'fecha_fin']
        widgets = {
            'fecha_inicio': forms.DateInput(
                attrs={'type': 'date', 'class': 'form-control'},
                format='%Y-%m-%d'
            ),
            'hora_inicio': forms.TimeInput(
                attrs={'type': 'time', 'class': 'form-control'},
                format='%H:%M'
            ),
            'fecha_fin': forms.DateTimeInput(
                attrs={'type': 'datetime-local', 'class': 'form-control'},
                format='%Y-%m-%dT%H:%M'
            ),
        }

Widget personalizado simple

Para casos más complejos, extiende la clase Widget:

from django.forms import Widget
from django.utils.html import format_html

class EstrellaRatingWidget(Widget):
    """Widget para seleccionar una puntuación de 1 a 5 estrellas."""
    template_name = 'widgets/estrellas.html'

    def __init__(self, maximo=5, attrs=None):
        self.maximo = maximo
        super().__init__(attrs)

    def get_context(self, name, value, attrs):
        context = super().get_context(name, value, attrs)
        context['widget']['maximo'] = range(1, self.maximo + 1)
        context['widget']['valor_actual'] = int(value) if value else 0
        return context

    def value_from_datadict(self, data, files, name):
        return data.get(name)
<!-- templates/widgets/estrellas.html -->
<div class="estrellas-widget">
    {% for i in widget.maximo %}
        <input type="radio"
               name="{{ widget.name }}"
               value="{{ i }}"
               id="{{ widget.attrs.id }}_{{ i }}"
               {% if widget.valor_actual == i %}checked{% endif %}>
        <label for="{{ widget.attrs.id }}_{{ i }}" title="{{ i }} estrellas">★</label>
    {% endfor %}
</div>

MultiWidget: campo con múltiples inputs

MultiWidget combina varios widgets en un único campo del formulario:

from django.forms import MultiWidget, TextInput, Select

class TelefonoWidget(MultiWidget):
    """Campo de teléfono con prefijo de país separado."""
    def __init__(self, attrs=None):
        widgets = [
            Select(
                choices=[('34', '+34 España'), ('1', '+1 EEUU'), ('44', '+44 UK')],
                attrs={'class': 'form-select telefono-prefijo'}
            ),
            TextInput(attrs={'class': 'form-control', 'placeholder': '123 456 789'}),
        ]
        super().__init__(widgets, attrs)

    def decompress(self, value):
        """Divide el valor almacenado en partes para cada widget."""
        if value:
            partes = value.split('-', 1)
            return partes if len(partes) == 2 else [None, value]
        return [None, None]

class TelefonoField(forms.MultiValueField):
    widget = TelefonoWidget

    def __init__(self, **kwargs):
        fields = [
            forms.ChoiceField(choices=[('34', '+34'), ('1', '+1'), ('44', '+44')]),
            forms.CharField(max_length=20),
        ]
        super().__init__(fields=fields, **kwargs)

    def compress(self, data_list):
        """Combina las partes en un único valor para guardar."""
        if data_list:
            return f'{data_list[0]}-{data_list[1]}'
        return ''

HiddenInput para datos ocultos

class PedidoForm(forms.ModelForm):
    class Meta:
        model = Pedido
        fields = ['producto', 'cantidad', 'sesion_id']
        widgets = {
            'sesion_id': forms.HiddenInput(),
        }

    def __init__(self, *args, sesion_id=None, **kwargs):
        super().__init__(*args, **kwargs)
        if sesion_id:
            self.initial['sesion_id'] = sesion_id

Los widgets son la capa de presentación del sistema de formularios de Django. Controlarlos correctamente permite integrar formularios con cualquier framework CSS y crear componentes de UI reutilizables sin modificar la lógica de validación.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Configurar widgets integrados de Django como DateInput, Select y CheckboxSelectMultiple. Añadir clases CSS y atributos HTML a los widgets con el parámetro attrs. Crear widgets personalizados extendiendo la clase Widget o MultiWidget. Usar HiddenInput, SplitDateTimeWidget y otros widgets avanzados. Controlar el renderizado de formularios con Form.renderer y plantillas personalizadas.