Formulario
Etiqueta, texto de ayuda, mensaje de error y el campo de texto, solo con clases y sin JavaScript. El campo
que no pasó la validación se marca con aria-invalid="true", el atributo que el lector de pantalla anuncia y que Django 5
escribe solo — así {{ field }} sale en rojo sin que nadie escriba nada.
Tal como figura en el registro fiscal.
Escribe un correo completo.
Puedes cambiarlo después, sin penalización.
Ejemplos
Las mismas cuatro clases en situaciones distintas.
Obligatorio
is-required en la etiqueta pone el asterisco; el required va en el campo.
Validación del navegador
Se pinta con :user-invalid, solo después de tocar el campo. Escribe algo sin @ y sal.
Con icono
.tuc-input-group con un SVG antes del campo.
Desactivado
disabled nativo; nada más.
Lista corta
data-tuc-select con data-search="false": sin campo de búsqueda, y con el error de aria-invalid.
Elige una provincia.
Varias opciones
Con multiple las elecciones se vuelven etiquetas, y getlist() sigue recibiendo una por opción.
El error vale en los componentes
El mismo aria-invalid="true" pinta los campos que el script sustituye por un control propio. El atributo se queda en el
elemento nativo, y el CSS llega al control a partir de él.
Elige el cliente.
La fecha tiene que ser posterior a hoy.
CNPJ no válido.
Ese color ya está en uso.
Obligatorio para continuar.
Cómo usar
Etiqueta con for, y ayuda y error unidos al campo con aria-describedby. Eso es lo que hace que el
lector de pantalla lea la ayuda y el error junto con el nombre del campo.
<label class="tuc-label is-required" for="doc">CNPJ</label>
<input id="doc" name="doc" data-tuc-mask="cnpj" required aria-invalid="true" aria-describedby="doc-e">
<p class="tuc-error" id="doc-e">CNPJ no válido.</p>
<label class="tuc-label" for="plan">Plan</label>
<select id="plan" name="plan" data-tuc-select aria-describedby="plan-h">
<option>Mensual</option>
<option>Anual</option>
</select>
<p class="tuc-hint" id="plan-h">Puedes cambiarlo después.</p>
<textarea class="tuc-input" name="notes" rows="3"></textarea>Los componentes de campo — date picker, máscara, color picker — se ponen la .tuc-input solos. Escribe la clase en los
campos comunes, para que toda la fila del formulario tenga la misma altura.
En la plantilla de Django
{# Django 5 ya pone aria-invalid y aria-describedby en el campo #}
<label class="tuc-label{% if field.field.required %} is-required{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<p class="tuc-hint" id="{{ field.auto_id }}_helptext">{{ field.help_text }}</p>{% endif %}
{% for error in field.errors %}<p class="tuc-error">{{ error }}</p>{% endfor %}El id de la ayuda es el que Django 5 indica en el aria-describedby del campo, <id del campo>_helptext.
Con este fragmento en un {% include %}, todos los campos del proyecto salen con la misma etiqueta, ayuda y error.
En el formulario de Django
class CustomerForm(forms.ModelForm):
class Meta:
model = Customer
fields = ["company", "email", "plan", "notes"]
widgets = {
"company": forms.TextInput(attrs={"class": "tuc-input"}),
"email": forms.EmailInput(attrs={"class": "tuc-input"}),
"plan": forms.Select(attrs={"data-tuc-select": ""}),
"notes": forms.Textarea(attrs={"class": "tuc-input", "rows": 3}),
}En JavaScript
No hay componente: para marcar un error que solo descubre el cliente, escribe el atributo, y bórralo cuando se corrija.
const field = document.querySelector('#email');
field.setAttribute('aria-invalid', 'true');
field.setAttribute('aria-describedby', 'email-e');
// corregido:
field.removeAttribute('aria-invalid');Por qué un atributo, y no una clase
Una clase roja solo sirve a quien ve. aria-invalid sirve a la pantalla y al lector de pantalla al mismo
tiempo, y por eso los dos no pueden divergir.
Desde Django 5.0 el formulario escribe el atributo en cada campo que volvió con error. En los componentes que sustituyen el campo
por un control propio — select, color picker, editor, upload — el atributo sigue en el nativo, y el CSS llega al control
con select[aria-invalid] + .tuc-select y :has(). Ningún componente copia el atributo, y por eso ninguno
se desincroniza cuando HTMX sustituye el campo. Incluso antes de que el script se monte, el campo en bruto ya nace con el borde rojo.
La validación del navegador pinta, pero solo después de tocar el campo
Un campo required vacío o un type="email" sin @ se ponen rojos con :user-invalid cuando la persona sale de él o intenta enviar — y no al cargar la página, que nacería toda roja. Un navegador que no conoce el selector simplemente no pinta este caso.
Verde cuando pasa, solo donde se pidió
Con data-validate en un .tuc-input nativo — correo, required, pattern —, el campo se pone verde con :user-valid después de que la persona lo complete bien. Sin el atributo no cambia nada: un formulario entero en verde es ruido. En la máscara con data-validate el verde aparece mientras se escribe. El error gana: un campo con aria-invalid="true" nunca se pone verde.
Con foco el anillo también queda rojo, y no vuelve al color de acento: el error sigue valiendo mientras se corrige. En la casilla y en la
opción el error aparece solo mientras están desmarcadas — marcadas, el color de acento dice más que un error que dejó de valer.
El asterisco de is-required es solo dibujo: el lector de pantalla ya anuncia el required, y oír "asterisco"
después del nombre estorbaría.
Convive con el CSS del proyecto
label e input[type=text] están entre las etiquetas que todo proyecto estiliza, y .card label le ganaría a una clase sola. Las clases de aquí se repiten en el selector para pesar más, así la etiqueta y el campo no se desarman dentro de las tarjetas de tu layout.
Clases
Solo CSS. Ninguna necesita JavaScript.
| Clase o atributo | Dónde | Para qué |
|---|---|---|
tuc-label | <label> | Etiqueta encima del campo |
is-required | con tuc-label | Asterisco después del nombre, oculto para el lector de pantalla |
tuc-hint | debajo del campo | Texto de ayuda |
tuc-error | debajo del campo | Mensaje de error |
tuc-input | <input> <textarea> <select> | Campo con la altura, el borde y el foco de la biblioteca; en el <select>, la flecha |
tuc-input-group | alrededor del campo | Deja espacio para un <svg> a la izquierda |
aria-invalid="true" | en el campo nativo | Borde y anillo de error, en cualquier campo de la biblioteca |
:user-invalid | automático | Error de la validación del navegador, después de que la persona toque el campo |
tuc-invalid | <input> | La pone la máscara al fallar la validación, junto con aria-invalid; no la escribas a mano |