Tucano v0.37.2

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 atributoDóndePara qué
tuc-label<label>Etiqueta encima del campo
is-requiredcon tuc-labelAsterisco después del nombre, oculto para el lector de pantalla
tuc-hintdebajo del campoTexto de ayuda
tuc-errordebajo del campoMensaje 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-groupalrededor del campoDeja espacio para un <svg> a la izquierda
aria-invalid="true"en el campo nativoBorde y anillo de error, en cualquier campo de la biblioteca
:user-invalidautomáticoError 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