Formulários acessíveis: além do aria-label, o que realmente importa

1. A base semântica do HTML para formulários acessíveis

A acessibilidade em formulários começa muito antes de qualquer atributo ARIA. A base está no HTML semântico correto, que muitas vezes é negligenciado em favor de soluções mais "modernas" como aria-label.

O elemento <label> com o atributo for continua sendo a maneira mais robusta de associar um rótulo a um campo. Diferente de aria-label, que substitui o texto visível, o <label> mantém a relação explícita entre o texto e o input, beneficiando usuários de leitores de tela e também aqueles que usam interfaces de voz.

<!-- Correto: label com for -->
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome">

<!-- Evitar: aria-label sem label visível -->
<input type="text" id="nome" name="nome" aria-label="Nome completo">

Para agrupamento lógico, <fieldset> e <legend> são indispensáveis. Eles informam ao leitor de tela que um conjunto de campos pertence a uma mesma categoria, como dados de endereço ou preferências de contato.

<fieldset>
  <legend>Informações de contato</legend>
  <label for="email">E-mail</label>
  <input type="email" id="email" name="email">
  <label for="tel">Telefone</label>
  <input type="tel" id="tel" name="tel">
</fieldset>

A escolha do tipo de input também impacta a acessibilidade. type="email" ativa teclados específicos em dispositivos móveis e validação nativa no navegador. type="tel" exibe um teclado numérico em smartphones. type="number" oferece controles de incremento/decremento que funcionam por teclado.

2. Gerenciamento de estado e feedback em tempo real

Usuários de tecnologias assistivas precisam saber imediatamente quando um campo é inválido e qual o erro. Depender apenas de bordas vermelhas ou ícones visuais é insuficiente.

aria-describedby associa uma mensagem de erro ao campo, enquanto aria-invalid sinaliza o estado de erro para leitores de tela.

<label for="senha">Senha</label>
<input type="password" id="senha" name="senha" 
       aria-describedby="erro-senha" 
       aria-invalid="true">
<span id="erro-senha" role="alert">A senha deve ter no mínimo 8 caracteres.</span>

Para feedback dinâmico sem interromper o fluxo, aria-live é essencial. aria-live="polite" espera o usuário terminar a ação atual para anunciar a mensagem, enquanto aria-live="assertive" interrompe imediatamente.

<div aria-live="polite" aria-atomic="true">
  <span id="status-cadastro"></span>
</div>

3. Navegação por teclado e foco gerenciado

A ordem de tabulação deve seguir a ordem visual do formulário. Evite tabindex positivo, que pode criar sequências confusas. Use tabindex="0" para elementos naturalmente não focáveis e tabindex="-1" para focar programaticamente.

O foco visível é obrigatório. Nunca remova o outline sem fornecer uma alternativa clara com :focus-visible.

input:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

Em modais de confirmação ou validação, evite armadilhas de foco. Quando um modal abre, o foco deve ir para o primeiro elemento interativo. Quando fecha, deve retornar ao elemento que o acionou.

// Exemplo conceitual de gerenciamento de foco
function abrirModal() {
  const modal = document.getElementById('modal-confirmacao');
  modal.style.display = 'block';
  document.getElementById('btn-confirmar').focus();
}

function fecharModal() {
  const modal = document.getElementById('modal-confirmacao');
  modal.style.display = 'none';
  document.getElementById('btn-abrir-modal').focus();
}

O uso de accesskey deve ser cauteloso. Pode conflitar com atalhos do navegador ou de leitores de tela. Prefira fornecer atalhos de teclado documentados e opcionais.

4. Validação nativa vs. validação customizada

A validação HTML5 nativa (required, pattern, minlength, maxlength) já oferece suporte básico a leitores de tela, mas as mensagens padrão variam entre navegadores e nem sempre são claras.

A Constraint Validation API permite personalizar mensagens sem perder a acessibilidade.

const campoEmail = document.getElementById('email');
campoEmail.addEventListener('invalid', function(event) {
  event.preventDefault();
  if (this.validity.valueMissing) {
    this.setCustomValidity('O campo de e-mail é obrigatório.');
  } else if (this.validity.typeMismatch) {
    this.setCustomValidity('Insira um e-mail válido, como exemplo@dominio.com.');
  }
  this.reportValidity();
});

Evite alert() para erros de validação. Prefira feedback inline com aria-errormessage, que substitui aria-describedby quando o campo está em estado de erro.

<label for="cpf">CPF</label>
<input type="text" id="cpf" name="cpf" 
       aria-errormessage="erro-cpf" 
       aria-invalid="true">
<span id="erro-cpf" role="alert">CPF inválido. Formato esperado: 000.000.000-00.</span>

5. Suporte a tecnologias assistivas além do leitor de tela

Acessibilidade não se resume a leitores de tela. Usuários com baixa visão precisam de contraste adequado (pelo menos 4.5:1 para texto normal) e tamanho de fonte ajustável sem quebra do layout.

Para mobilidade reduzida, alvos de toque devem ter no mínimo 44x44 pixels (recomendação WCAG 2.2). Botões e campos muito próximos podem causar erros de clique.

button, input[type="submit"], input[type="reset"] {
  min-height: 44px;
  min-width: 44px;
  padding: 12px 16px;
}

O atributo autocomplete melhora significativamente a experiência para usuários com deficiências cognitivas e motoras, permitindo preenchimento automático.

<input type="text" id="endereco" name="endereco" autocomplete="street-address">
<input type="text" id="cidade" name="cidade" autocomplete="address-level2">
<input type="text" id="cep" name="cep" autocomplete="postal-code">

6. Internacionalização e localização em formulários acessíveis

Placeholder nunca substitui label. Placeholder desaparece ao digitar, não é acessível por padrão em todos os leitores de tela e tem baixo contraste.

<!-- ERRADO: placeholder como único rótulo -->
<input type="text" placeholder="Digite seu nome">

<!-- CORRETO: label visível -->
<label for="nome">Nome completo</label>
<input type="text" id="nome" placeholder="Ex: Maria Silva">

Para formatação de dados, use inputmode para ativar o teclado correto em dispositivos móveis sem alterar o tipo do input.

<input type="text" id="moeda" inputmode="decimal" pattern="[0-9]+([.,][0-9]{1,2})?" 
       lang="pt-BR" aria-label="Valor em reais">

Em formulários multilíngues, use dir="rtl" para idiomas da direita para a esquerda e defina lang no elemento <html> ou em partes específicas do formulário.

<div lang="ar" dir="rtl">
  <label for="nome-ar">الاسم الكامل</label>
  <input type="text" id="nome-ar">
</div>

7. Testes práticos de acessibilidade em formulários

Ferramentas automatizadas como axe DevTools e Lighthouse capturam problemas básicos, mas não detectam tudo. Um formulário pode passar em todos os testes automatizados e ainda ser inacessível.

Testes manuais são indispensáveis:

  1. Navegue por todo o formulário usando apenas a tecla Tab.
  2. Verifique se o foco é visível em todos os elementos interativos.
  3. Teste com leitor de tela (NVDA no Windows, VoiceOver no Mac, TalkBack no Android).
  4. Confirme que mensagens de erro são anunciadas automaticamente.
  5. Verifique a ordem de tabulação com o formulário em diferentes tamanhos de tela.

Checklist de auditoria para formulários:

  • [ ] Todos os campos têm <label> visível e acessível
  • [ ] Mensagens de erro usam aria-describedby ou aria-errormessage
  • [ ] Foco visível em todos os elementos interativos
  • [ ] Ordem de tabulação segue a ordem visual
  • [ ] Contraste de texto e ícones atende WCAG AA
  • [ ] Alvos de toque têm no mínimo 44x44 pixels
  • [ ] autocomplete configurado para campos comuns
  • [ ] Formulário funcional apenas com teclado

Referências