Tucano v0.37.2

Formulário

Rótulo, texto de ajuda, mensagem de erro e o campo de texto, só com classes e sem JavaScript. O campo que reprovou é marcado por aria-invalid="true", o atributo que o leitor de tela anuncia e que o Django 5 escreve sozinho — então {{ field }} sai vermelho sem ninguém escrever nada.

Como está no cartão CNPJ.

Informe um e-mail completo.

Dá para trocar depois, sem multa.

Exemplos

As mesmas quatro classes em situações diferentes.

Obrigatório

is-required no rótulo põe o asterisco; o required fica no campo.

Validação do navegador

Pinta por :user-invalid, só depois de mexer. Digite algo sem @ e saia.

Com ícone

.tuc-input-group com um SVG antes do campo.

Desativado

disabled nativo; nada a mais.

Lista curta

data-tuc-select com data-search="false": sem campo de busca, e com o erro do aria-invalid.

Escolha um estado.

Várias escolhas

Com multiple as escolhas viram tags, e o getlist() continua recebendo uma por opção.

O erro vale nos componentes

O mesmo aria-invalid="true" pinta os campos que o script troca por um controle próprio. O atributo fica no elemento nativo, e o CSS alcança o controle a partir dele.

Escolha o cliente.

A data precisa ser depois de hoje.

CNPJ inválido.

Essa cor já está em uso.

Obrigatório para continuar.

Como usar

Rótulo com for, ajuda e erro ligados ao campo por aria-describedby. É isso que faz o leitor de tela ler a ajuda e o erro junto com o nome do 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 inválido.</p>

<label class="tuc-label" for="plan">Plano</label>
<select id="plan" name="plan" data-tuc-select aria-describedby="plan-h">
  <option>Mensal</option>
  <option>Anual</option>
</select>
<p class="tuc-hint" id="plan-h">Dá para trocar depois.</p>

<textarea class="tuc-input" name="notes" rows="3"></textarea>

Os componentes de campo — date picker, máscara, color picker — vestem a .tuc-input sozinhos. Escreva a classe nos campos comuns, para a linha inteira do formulário ficar da mesma altura.

No template do Django

{# o Django 5 já põe aria-invalid e aria-describedby no 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 %}

O id da ajuda é o que o Django 5 aponta no aria-describedby do campo, <id do campo>_helptext. Com esse trecho num {% include %}, todo campo do projeto sai com rótulo, ajuda e erro iguais.

No formulário do 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}),
        }

Em JavaScript

Não há componente: para marcar um erro que só o cliente descobre, escreva o atributo, e apague quando corrigir.

const field = document.querySelector('#email');
field.setAttribute('aria-invalid', 'true');
field.setAttribute('aria-describedby', 'email-e');
// corrigido:
field.removeAttribute('aria-invalid');

Por que atributo, e não classe

Uma classe vermelha só serve a quem vê. O aria-invalid serve à tela e ao leitor de tela ao mesmo tempo, e por isso as duas não têm como divergir.

Desde o Django 5.0 o formulário escreve o atributo em todo campo que voltou com erro. Nos componentes que trocam o campo por um controle próprio — select, color picker, editor, upload — o atributo continua no nativo, e o CSS chega ao controle por select[aria-invalid] + .tuc-select e :has(). Nenhum componente copia o atributo, e por isso nenhum fica dessincronizado quando o HTMX troca o campo. Até antes de o script montar, o campo cru já nasce com a borda vermelha.

A validação do navegador pinta, mas só depois de mexer

Um campo required vazio ou um type="email" sem @ ficam vermelhos por :user-invalid quando a pessoa sai dele ou tenta enviar — e não ao carregar a página, que nasceria toda vermelha. Navegador que não conhece o seletor só deixa de pintar esse caso.

Verde quando passa, só em quem pediu

Com data-validate num .tuc-input nativo — e-mail, required, pattern —, o campo fica verde por :user-valid depois de a pessoa preenchê-lo certo. Sem o atributo, nada muda: um formulário inteiro verde vira ruído. Na máscara com data-validate o verde aparece já enquanto se digita. Erro vence: campo com aria-invalid="true" nunca fica verde.

Em foco o anel também fica vermelho, e não volta ao destaque: o erro continua valendo enquanto se corrige. Na caixa e na opção o erro aparece só enquanto estão desmarcadas — marcada, a cor de destaque diz mais que um erro que deixou de valer. O asterisco do is-required é só desenho: o leitor de tela já anuncia o required, e ouvir "asterisco" depois do nome atrapalharia.

Convive com o CSS do projeto

label e input[type=text] são das tags que todo projeto estiliza, e .card label venceria uma classe sozinha. As classes daqui se repetem no seletor para pesar mais, então o rótulo e o campo não se desmontam dentro dos cartões do seu layout.

Classes

Só CSS. Nenhuma delas precisa de JavaScript.

Classe ou atributoOndePara quê
tuc-label<label>Rótulo acima do campo
is-requiredcom tuc-labelAsterisco depois do nome, escondido do leitor de tela
tuc-hintembaixo do campoTexto de ajuda
tuc-errorembaixo do campoMensagem de erro
tuc-input<input> <textarea> <select>Campo com a altura, a borda e o foco da biblioteca; no <select>, a seta
tuc-input-groupem volta do campoAbre espaço para um <svg> à esquerda
aria-invalid="true"no campo nativoBorda e anel de erro, em qualquer campo da biblioteca
:user-invalidautomáticoErro da validação do navegador, depois de a pessoa mexer
tuc-invalid<input>Posta pela máscara ao reprovar, junto do aria-invalid; não escreva à mão