Acessibilidade

Formulários Acessíveis

Como criar formulários que todos podem usar facilmente.

Por que Formulários Acessíveis?

Formulários são uma das partes mais importantes e desafiadoras da acessibilidade web. Eles devem ser:

  • Claros: Fáceis de entender e preencher
  • Navegáveis: Acessíveis via teclado
  • Informativos: Fornecem feedback claro sobre erros
  • Eficientes: Minimizam esforço do usuário

Labels e Associações

Labels Explícitos

<!-- ✅ Bom - label associado via for/id -->
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome">

Labels Implícitos

<!-- ✅ Também válido - label envolve o input -->
<label>
  Nome completo
  <input type="text" name="nome">
</label>

Nunca Sem Label

<!-- ❌ Ruim - sem label -->
<input type="text" placeholder="Nome">

<!-- ✅ Bom - com label -->
<label for="nome">Nome</label>
<input type="text" id="nome" placeholder="Nome completo">

Campos Obrigatórios

Indicadores Visuais e Semânticos

<!-- ✅ Bom - múltiplos indicadores -->
<label for="email">
  Email
  <span aria-label="campo obrigatório">*</span>
  <span class="sr-only">obrigatório</span>
</label>
<input 
  type="email" 
  id="email" 
  name="email"
  required
  aria-required="true"
  aria-describedby="email-hint">
<span id="email-hint" class="hint">Seu endereço de email</span>
/* Estilo para campo obrigatório */
label span[aria-label="campo obrigatório"] {
  color: #d32f2f;
  font-weight: bold;
}

/* Screen reader only */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Agrupamento de Campos

Fieldset e Legend

<!-- ✅ Bom - agrupamento semântico -->
<fieldset>
  <legend>Informações de Contato</legend>
  
  <label for="telefone">Telefone</label>
  <input type="tel" id="telefone" name="telefone">
  
  <label for="celular">Celular</label>
  <input type="tel" id="celular" name="celular">
</fieldset>

<fieldset>
  <legend>Endereço</legend>
  
  <label for="rua">Rua</label>
  <input type="text" id="rua" name="rua">
  
  <label for="cidade">Cidade</label>
  <input type="text" id="cidade" name="cidade">
</fieldset>

Dicas e Instruções

aria-describedby

<label for="senha">Senha</label>
<input 
  type="password" 
  id="senha" 
  name="senha"
  aria-describedby="senha-hint senha-requisitos">
<span id="senha-hint" class="hint">
  Mínimo de 8 caracteres
</span>
<ul id="senha-requisitos" class="sr-only">
  <li>Pelo menos 8 caracteres</li>
  <li>Uma letra maiúscula</li>
  <li>Um número</li>
</ul>

Instruções Visíveis

<label for="username">Nome de usuário</label>
<input 
  type="text" 
  id="username" 
  name="username"
  aria-describedby="username-hint">
<p id="username-hint" class="hint">
  Use apenas letras, números e underscore. Mínimo 3 caracteres.
</p>

Validação e Erros

Mensagens de Erro Acessíveis

<form novalidate>
  <label for="email">Email</label>
  <input 
    type="email" 
    id="email" 
    name="email"
    aria-invalid="false"
    aria-describedby="email-error"
    required>
  <span 
    id="email-error" 
    class="error-message" 
    role="alert"
    aria-live="polite">
    <!-- Mensagem de erro aparecerá aqui -->
  </span>
</form>
function validateEmail(input) {
  const email = input.value;
  const errorElement = document.getElementById('email-error');
  
  if (!email) {
    input.setAttribute('aria-invalid', 'true');
    errorElement.textContent = 'Email é obrigatório';
    return false;
  }
  
  if (!isValidEmail(email)) {
    input.setAttribute('aria-invalid', 'true');
    errorElement.textContent = 'Email inválido. Use o formato exemplo@dominio.com';
    return false;
  }
  
  input.setAttribute('aria-invalid', 'false');
  errorElement.textContent = '';
  return true;
}

input.addEventListener('blur', () => validateEmail(input));

Estilos de Erro

input[aria-invalid="true"] {
  border: 2px solid #d32f2f;
  background-color: #ffebee;
}

.error-message {
  color: #d32f2f;
  font-size: 0.875rem;
  margin-top: 0.25rem;
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.error-message::before {
  content: "⚠️";
  aria-hidden: "true";
}

Tipos de Input Apropriados

Use Tipos Semânticos

<!-- ✅ Bom - tipos específicos -->
<label for="email">Email</label>
<input type="email" id="email" name="email">

<label for="telefone">Telefone</label>
<input type="tel" id="telefone" name="telefone">

<label for="data">Data de nascimento</label>
<input type="date" id="data" name="data">

<label for="url">Website</label>
<input type="url" id="url" name="url">

<label for="numero">Quantidade</label>
<input type="number" id="numero" name="numero" min="1" max="100">

Autocomplete

<!-- ✅ Bom - ajuda preenchimento automático -->
<label for="nome-completo">Nome completo</label>
<input 
  type="text" 
  id="nome-completo" 
  name="nome-completo"
  autocomplete="name">

<label for="endereco">Endereço</label>
<input 
  type="text" 
  id="endereco" 
  name="endereco"
  autocomplete="street-address">

<label for="cep">CEP</label>
<input 
  type="text" 
  id="cep" 
  name="cep"
  autocomplete="postal-code">

Seleções e Checkboxes

Radio Buttons

<!-- ✅ Bom - agrupado com fieldset -->
<fieldset>
  <legend>Método de pagamento</legend>
  
  <input 
    type="radio" 
    id="cartao" 
    name="pagamento" 
    value="cartao">
  <label for="cartao">Cartão de crédito</label>
  
  <input 
    type="radio" 
    id="boleto" 
    name="pagamento" 
    value="boleto">
  <label for="boleto">Boleto</label>
  
  <input 
    type="radio" 
    id="pix" 
    name="pagamento" 
    value="pix">
  <label for="pix">PIX</label>
</fieldset>

Checkboxes

<!-- ✅ Bom -->
<fieldset>
  <legend>Interesses (selecione todos que se aplicam)</legend>
  
  <input 
    type="checkbox" 
    id="tecnologia" 
    name="interesses" 
    value="tecnologia">
  <label for="tecnologia">Tecnologia</label>
  
  <input 
    type="checkbox" 
    id="design" 
    name="interesses" 
    value="design">
  <label for="design">Design</label>
</fieldset>

Selects Acessíveis

Select Simples

<label for="pais">País</label>
<select id="pais" name="pais">
  <option value="">Selecione um país</option>
  <option value="br">Brasil</option>
  <option value="us">Estados Unidos</option>
  <option value="pt">Portugal</option>
</select>

Select com Grupos

<label for="cidade">Cidade</label>
<select id="cidade" name="cidade">
  <optgroup label="São Paulo">
    <option value="sp-capital">São Paulo - Capital</option>
    <option value="sp-campinas">Campinas</option>
  </optgroup>
  <optgroup label="Rio de Janeiro">
    <option value="rj-capital">Rio de Janeiro - Capital</option>
    <option value="rj-niteroi">Niterói</option>
  </optgroup>
</select>

Botões de Envio

Botões Descritivos

<!-- ✅ Bom - texto descritivo -->
<button type="submit">
  Criar conta
</button>

<!-- ✅ Melhor - com ícone e texto -->
<button type="submit">
  <span aria-hidden="true"></span>
  Criar conta
</button>

Estados de Loading

<button type="submit" aria-busy="false" id="submit-btn">
  Enviar formulário
</button>
button.addEventListener('click', async function() {
  button.setAttribute('aria-busy', 'true');
  button.disabled = true;
  button.textContent = 'Enviando...';
  
  try {
    await submitForm();
    button.textContent = 'Enviado com sucesso!';
  } catch (error) {
    button.setAttribute('aria-busy', 'false');
    button.disabled = false;
    button.textContent = 'Enviar formulário';
    // Mostrar erro
  }
});

Exemplo Completo

<form novalidate aria-label="Formulário de contato">
  <fieldset>
    <legend>Informações Pessoais</legend>
    
    <div>
      <label for="nome">
        Nome completo
        <span aria-label="obrigatório">*</span>
      </label>
      <input 
        type="text" 
        id="nome" 
        name="nome"
        required
        aria-required="true"
        autocomplete="name">
    </div>
    
    <div>
      <label for="email">
        Email
        <span aria-label="obrigatório">*</span>
      </label>
      <input 
        type="email" 
        id="email" 
        name="email"
        required
        aria-required="true"
        aria-describedby="email-hint email-error"
        autocomplete="email">
      <span id="email-hint" class="hint">
        Usaremos este email para contato
      </span>
      <span id="email-error" class="error-message" role="alert"></span>
    </div>
  </fieldset>
  
  <fieldset>
    <legend>Mensagem</legend>
    
    <div>
      <label for="assunto">Assunto</label>
      <input type="text" id="assunto" name="assunto">
    </div>
    
    <div>
      <label for="mensagem">Mensagem</label>
      <textarea 
        id="mensagem" 
        name="mensagem"
        rows="5"
        aria-describedby="mensagem-hint"></textarea>
      <span id="mensagem-hint" class="hint">
        Descreva sua dúvida ou solicitação
      </span>
    </div>
  </fieldset>
  
  <button type="submit">Enviar mensagem</button>
</form>
Teste seus formulários com leitores de tela e apenas teclado. Isso revelará problemas que você pode não notar visualmente.