Formulários são uma das partes mais importantes e desafiadoras da acessibilidade web. Eles devem ser:
<!-- ✅ Bom - label associado via for/id -->
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome">
<!-- ✅ Também válido - label envolve o input -->
<label>
Nome completo
<input type="text" name="nome">
</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">
<!-- ✅ 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;
}
<!-- ✅ 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>
<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>
<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>
<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));
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";
}
<!-- ✅ 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">
<!-- ✅ 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">
<!-- ✅ 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>
<!-- ✅ 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>
<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>
<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>
<!-- ✅ 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>
<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
}
});
<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>