Domine técnicas avançadas de formulários: validação complexa, fieldsets dinâmicos, UX otimizada para conversão e integrações modernas.
Embora a validação HTML5 seja útil, formulários profissionais precisam de validação mais sofisticada, feedback em tempo real e integração com APIs.
Validação avançada é como ter um "assistente pessoal inteligente" no seu formulário. Enquanto HTML5 básico apenas diz "campo obrigatório", validação avançada guia o usuário, previne erros e melhora a experiência.
Por que ir além do HTML5:
Estatística impressionante: 67% dos usuários abandonam formulários após o primeiro erro. Validação em tempo real reduz abandono para 23%!
Filosofia de design: "Prevenir é melhor que corrigir". Ajude o usuário a acertar na primeira tentativa.
Validação em tempo real é como ter um "GPS para formulários" - corrige a rota antes que o usuário se perca. É a diferença entre descobrir erros ao final (frustrante) vs ser guiado durante o preenchimento (agradável).
Momentos ideais para validar:
Tipos de feedback visual:
Dica profissional: Use novalidate
no form para
desabilitar validação nativa e ter controle total. Implemente sua própria
lógica mais inteligente.
<form id="registration-form" novalidate>
<div class="form-group">
<label for="email-check">E-mail</label>
<div class="input-container">
<input type="email"
id="email-check"
name="email"
required
data-validate="email">
<div class="validation-icon"></div>
</div>
<div class="validation-message"></div>
</div>
<div class="form-group">
<label for="password-strength">Senha</label>
<div class="input-container">
<input type="password"
id="password-strength"
name="password"
required
data-validate="password">
<div class="password-toggle" onclick="togglePassword()">👁️</div>
</div>
<div class="password-strength">
<div class="strength-meter">
<div class="strength-fill"></div>
</div>
<div class="strength-text">Força da senha</div>
</div>
<div class="validation-message"></div>
</div>
<div class="form-group">
<label for="cpf-validate">CPF</label>
<div class="input-container">
<input type="text"
id="cpf-validate"
name="cpf"
required
data-validate="cpf"
data-mask="000.000.000-00">
<div class="validation-icon"></div>
</div>
<div class="validation-message"></div>
</div>
<button type="submit" disabled>Cadastrar</button>
</form>
<script>
class FormValidator {
constructor(form) {
this.form = form;
this.fields = {};
this.isValid = false;
this.init();
}
init() {
const inputs = this.form.querySelectorAll('[data-validate]');
inputs.forEach(input => {
this.setupField(input);
});
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
setupField(input) {
const type = input.dataset.validate;
this.fields[input.name] = { element: input, valid: false };
// Eventos
input.addEventListener('input', () => this.validateField(input, type));
input.addEventListener('blur', () => this.validateField(input, type));
// Máscara se especificada
if (input.dataset.mask) {
input.addEventListener('input', () => this.applyMask(input));
}
}
validateField(input, type) {
const value = input.value.trim();
let isValid = false;
let message = '';
switch (type) {
case 'email':
isValid = this.validateEmail(value);
message = isValid ? '✓ E-mail válido' : '✗ E-mail inválido';
break;
case 'password':
const strength = this.validatePassword(value);
isValid = strength.score >= 3;
message = strength.message;
this.updatePasswordStrength(input, strength);
break;
case 'cpf':
isValid = this.validateCPF(value);
message = isValid ? '✓ CPF válido' : '✗ CPF inválido';
break;
}
this.updateFieldStatus(input, isValid, message);
this.fields[input.name].valid = isValid;
this.updateSubmitButton();
}
validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
validatePassword(password) {
let score = 0;
let message = '';
if (password.length >= 8) score++;
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/\d/.test(password)) score++;
if (/[^a-zA-Z\d]/.test(password)) score++;
switch (score) {
case 0:
case 1:
message = 'Muito fraca';
break;
case 2:
message = 'Fraca';
break;
case 3:
message = 'Média';
break;
case 4:
message = 'Forte';
break;
case 5:
message = 'Muito forte';
break;
}
return { score, message };
}
validateCPF(cpf) {
cpf = cpf.replace(/[^\d]/g, '');
if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) {
return false;
}
// Validação dos dígitos verificadores
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += parseInt(cpf.charAt(i)) * (10 - i);
}
let remainder = (sum * 10) % 11;
if (remainder === 10 || remainder === 11) remainder = 0;
if (remainder !== parseInt(cpf.charAt(9))) return false;
sum = 0;
for (let i = 0; i < 10; i++) {
sum += parseInt(cpf.charAt(i)) * (11 - i);
}
remainder = (sum * 10) % 11;
if (remainder === 10 || remainder === 11) remainder = 0;
if (remainder !== parseInt(cpf.charAt(10))) return false;
return true;
}
applyMask(input) {
const mask = input.dataset.mask;
let value = input.value.replace(/[^\d]/g, '');
let maskedValue = '';
let maskIndex = 0;
for (let i = 0; i < mask.length && maskIndex < value.length; i++) {
if (mask[i] === '0') {
maskedValue += value[maskIndex];
maskIndex++;
} else {
maskedValue += mask[i];
}
}
input.value = maskedValue;
}
updateFieldStatus(input, isValid, message) {
const container = input.closest('.form-group');
const icon = container.querySelector('.validation-icon');
const messageEl = container.querySelector('.validation-message');
icon.textContent = isValid ? '✓' : '✗';
icon.className = `validation-icon ${isValid ? 'valid' : 'invalid'}`;
messageEl.textContent = message;
messageEl.className = `validation-message ${isValid ? 'valid' : 'invalid'}`;
input.setAttribute('aria-invalid', !isValid);
}
updatePasswordStrength(input, strength) {
const container = input.closest('.form-group');
const meter = container.querySelector('.strength-fill');
const text = container.querySelector('.strength-text');
const percentage = (strength.score / 5) * 100;
meter.style.width = `${percentage}%`;
meter.className = `strength-fill strength-${strength.score}`;
text.textContent = strength.message;
}
updateSubmitButton() {
const allValid = Object.values(this.fields).every(field => field.valid);
const submitBtn = this.form.querySelector('button[type="submit"]');
submitBtn.disabled = !allValid;
}
handleSubmit(e) {
e.preventDefault();
if (this.isFormValid()) {
this.submitForm();
}
}
isFormValid() {
return Object.values(this.fields).every(field => field.valid);
}
async submitForm() {
const submitBtn = this.form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = 'Enviando...';
submitBtn.disabled = true;
try {
// Simular envio
await new Promise(resolve => setTimeout(resolve, 2000));
submitBtn.textContent = '✓ Enviado!';
submitBtn.className = 'btn-success';
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.className = '';
submitBtn.disabled = false;
}, 3000);
} catch (error) {
submitBtn.textContent = '✗ Erro no envio';
submitBtn.className = 'btn-error';
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.className = '';
submitBtn.disabled = false;
}, 3000);
}
}
}
// Inicializar validador
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('registration-form');
new FormValidator(form);
});
function togglePassword() {
const input = document.getElementById('password-strength');
const toggle = document.querySelector('.password-toggle');
if (input.type === 'password') {
input.type = 'text';
toggle.textContent = '🙈';
} else {
input.type = 'password';
toggle.textContent = '👁️';
}
}
</script>
/* Container de input com ícone */
.input-container {
position: relative;
display: flex;
align-items: center;
}
.input-container input {
flex: 1;
padding-right: 40px;
}
.validation-icon {
position: absolute;
right: 12px;
font-weight: bold;
font-size: 16px;
transition: all 0.3s ease;
}
.validation-icon.valid {
color: #28a745;
}
.validation-icon.invalid {
color: #dc3545;
}
/* Mensagens de validação */
.validation-message {
margin-top: 4px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.validation-message.valid {
color: #28a745;
}
.validation-message.invalid {
color: #dc3545;
}
/* Medidor de força da senha */
.password-strength {
margin-top: 8px;
}
.strength-meter {
width: 100%;
height: 6px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.strength-fill {
height: 100%;
transition: all 0.3s ease;
border-radius: 3px;
}
.strength-fill.strength-0,
.strength-fill.strength-1 {
background-color: #dc3545;
}
.strength-fill.strength-2 {
background-color: #fd7e14;
}
.strength-fill.strength-3 {
background-color: #ffc107;
}
.strength-fill.strength-4 {
background-color: #20c997;
}
.strength-fill.strength-5 {
background-color: #28a745;
}
.strength-text {
font-size: 12px;
color: #6c757d;
}
/* Toggle de senha */
.password-toggle {
position: absolute;
right: 12px;
cursor: pointer;
font-size: 18px;
user-select: none;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.password-toggle:hover {
opacity: 1;
}
/* Estados do botão */
button[type="submit"] {
transition: all 0.3s ease;
}
button[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-success {
background-color: #28a745 !important;
}
.btn-error {
background-color: #dc3545 !important;
}
Crie um sistema de cadastro avançado com:
Formulários multi-etapas são como "subir uma escada" ao invés de escalar uma montanha. Dividem formulários longos em etapas digeríveis, reduzindo a sensação de sobrecarga e aumentando drasticamente a taxa de conclusão.
Quando usar formulários multi-etapas:
Estatísticas que impressionam:
Psicologia por trás: Progresso visível ativa sistema de recompensa do cérebro. Cada etapa completada gera sensação de conquista.
Uma estrutura multi-etapas bem projetada é como um "mapa de navegação" - o usuário sempre sabe onde está, para onde vai, e quanto falta para terminar.
Componentes essenciais:
Regras de ouro:
<form id="multi-step-form" class="multi-step-form">
<div class="progress-indicator">
<div class="step active" data-step="1">
<span class="step-number">1</span>
<span class="step-title">Dados Pessoais</span>
</div>
<div class="step" data-step="2">
<span class="step-number">2</span>
<span class="step-title">Endereço</span>
</div>
<div class="step" data-step="3">
<span class="step-number">3</span>
<span class="step-title">Preferências</span>
</div>
<div class="step" data-step="4">
<span class="step-number">4</span>
<span class="step-title">Confirmação</span>
</div>
</div>
<div class="form-step active" data-step="1">
<h2>👤 Dados Pessoais</h2>
<p>Vamos começar com suas informações básicas</p>
<div class="form-row">
<div class="form-col">
<label for="first-name">Nome</label>
<input type="text"
id="first-name"
name="firstName"
required
data-step="1">
</div>
<div class="form-col">
<label for="last-name">Sobrenome</label>
<input type="text"
id="last-name"
name="lastName"
required
data-step="1">
</div>
</div>
<div class="form-group">
<label for="email-step">E-mail</label>
<input type="email"
id="email-step"
name="email"
required
data-step="1">
</div>
<div class="form-group">
<label for="phone-step">Telefone</label>
<input type="tel"
id="phone-step"
name="phone"
required
data-step="1">
</div>
</div>
<div class="form-step" data-step="2">
<h2>🏠 Endereço</h2>
<p>Onde você mora?</p>
<div class="form-group">
<label for="cep-step">CEP</label>
<input type="text"
id="cep-step"
name="cep"
required
data-step="2"
onblur="buscarCEP(this.value)">
</div>
<div class="form-row">
<div class="form-col" style="flex: 3;">
<label for="street">Rua</label>
<input type="text"
id="street"
name="street"
required
data-step="2">
</div>
<div class="form-col" style="flex: 1;">
<label for="number">Número</label>
<input type="text"
id="number"
name="number"
required
data-step="2">
</div>
</div>
<div class="form-row">
<div class="form-col">
<label for="city">Cidade</label>
<input type="text"
id="city"
name="city"
required
data-step="2">
</div>
<div class="form-col">
<label for="state">Estado</label>
<select id="state" name="state" required data-step="2">
<option value="">Selecione</option>
<option value="SP">São Paulo</option>
<option value="RJ">Rio de Janeiro</option>
<option value="MG">Minas Gerais</option>
</select>
</div>
</div>
</div>
<div class="form-step" data-step="3">
<h2>⚙️ Preferências</h2>
<p>Como prefere receber comunicações?</p>
<fieldset>
<legend>Método de Contato Preferido</legend>
<label class="radio-card">
<input type="radio" name="contactMethod" value="email" data-step="3">
<div class="radio-card-content">
<h4>📧 E-mail</h4>
<p>Comunicação por e-mail</p>
</div>
</label>
<label class="radio-card">
<input type="radio" name="contactMethod" value="sms" data-step="3">
<div class="radio-card-content">
<h4>📱 SMS</h4>
<p>Mensagens de texto</p>
</div>
</label>
<label class="radio-card">
<input type="radio" name="contactMethod" value="phone" data-step="3">
<div class="radio-card-content">
<h4>📞 Telefone</h4>
<p>Ligações diretas</p>
</div>
</label>
</fieldset>
<fieldset>
<legend>Interesses</legend>
<div class="checkbox-group">
<label>
<input type="checkbox" name="interests[]" value="tech" data-step="3">
🖥️ Tecnologia
</label>
<label>
<input type="checkbox" name="interests[]" value="design" data-step="3">
🎨 Design
</label>
<label>
<input type="checkbox" name="interests[]" value="business" data-step="3">
💼 Negócios
</label>
</div>
</fieldset>
</div>
<div class="form-step" data-step="4">
<h2>✅ Confirmação</h2>
<p>Revise suas informações antes de enviar</p>
<div class="summary-section">
<h3>👤 Dados Pessoais</h3>
<div id="summary-personal"></div>
</div>
<div class="summary-section">
<h3>🏠 Endereço</h3>
<div id="summary-address"></div>
</div>
<div class="summary-section">
<h3>⚙️ Preferências</h3>
<div id="summary-preferences"></div>
</div>
<label class="checkbox-custom">
<input type="checkbox" name="terms" required data-step="4">
<span class="checkmark"></span>
Li e aceito os <a href="/termos" target="_blank">termos de uso</a>
</label>
</div>
<div class="form-navigation">
<button type="button" id="prev-btn" class="btn btn-secondary" onclick="previousStep()">
← Anterior
</button>
<button type="button" id="next-btn" class="btn btn-primary" onclick="nextStep()">
Próximo →
</button>
<button type="submit" id="submit-btn" class="btn btn-success" style="display: none;">
🚀 Finalizar Cadastro
</button>
</div>
</form>
class MultiStepForm {
constructor(formId) {
this.form = document.getElementById(formId);
this.currentStep = 1;
this.totalSteps = 4;
this.formData = {};
this.init();
}
init() {
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
this.updateStepDisplay();
this.bindEvents();
}
bindEvents() {
// Salvar dados automaticamente
const inputs = this.form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
input.addEventListener('change', () => this.saveStepData());
input.addEventListener('input', () => this.saveStepData());
});
}
nextStep() {
if (this.validateCurrentStep()) {
this.saveStepData();
if (this.currentStep < this.totalSteps) {
this.currentStep++;
this.updateStepDisplay();
if (this.currentStep === this.totalSteps) {
this.generateSummary();
}
}
}
}
previousStep() {
if (this.currentStep > 1) {
this.currentStep--;
this.updateStepDisplay();
}
}
updateStepDisplay() {
// Ocultar todas as etapas
const steps = this.form.querySelectorAll('.form-step');
steps.forEach(step => step.classList.remove('active'));
// Mostrar etapa atual
const currentStepEl = this.form.querySelector(`[data-step="${this.currentStep}"]`);
if (currentStepEl && currentStepEl.classList.contains('form-step')) {
currentStepEl.classList.add('active');
}
// Atualizar indicador de progresso
this.updateProgressIndicator();
// Atualizar botões de navegação
this.updateNavigationButtons();
// Scroll para o topo
this.form.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
updateProgressIndicator() {
const progressSteps = this.form.querySelectorAll('.progress-indicator .step');
progressSteps.forEach((step, index) => {
const stepNumber = index + 1;
if (stepNumber < this.currentStep) {
step.classList.add('completed');
step.classList.remove('active');
} else if (stepNumber === this.currentStep) {
step.classList.add('active');
step.classList.remove('completed');
} else {
step.classList.remove('active', 'completed');
}
});
}
updateNavigationButtons() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
// Botão anterior
prevBtn.style.display = this.currentStep === 1 ? 'none' : 'inline-block';
// Botão próximo/enviar
if (this.currentStep === this.totalSteps) {
nextBtn.style.display = 'none';
submitBtn.style.display = 'inline-block';
} else {
nextBtn.style.display = 'inline-block';
submitBtn.style.display = 'none';
}
}
validateCurrentStep() {
const currentInputs = this.form.querySelectorAll(`[data-step="${this.currentStep}"]`);
let isValid = true;
currentInputs.forEach(input => {
if (input.hasAttribute('required') && !input.value.trim()) {
this.showError(input, 'Este campo é obrigatório');
isValid = false;
} else if (input.type === 'email' && input.value && !this.isValidEmail(input.value)) {
this.showError(input, 'E-mail inválido');
isValid = false;
} else {
this.clearError(input);
}
});
return isValid;
}
showError(input, message) {
const group = input.closest('.form-group') || input.closest('.form-col');
let errorEl = group.querySelector('.error-message');
if (!errorEl) {
errorEl = document.createElement('div');
errorEl.className = 'error-message';
group.appendChild(errorEl);
}
errorEl.textContent = message;
input.classList.add('error');
}
clearError(input) {
const group = input.closest('.form-group') || input.closest('.form-col');
const errorEl = group.querySelector('.error-message');
if (errorEl) {
errorEl.remove();
}
input.classList.remove('error');
}
saveStepData() {
const currentInputs = this.form.querySelectorAll(`[data-step="${this.currentStep}"]`);
currentInputs.forEach(input => {
if (input.type === 'checkbox') {
if (!this.formData[input.name]) {
this.formData[input.name] = [];
}
if (input.checked && !this.formData[input.name].includes(input.value)) {
this.formData[input.name].push(input.value);
} else if (!input.checked) {
this.formData[input.name] = this.formData[input.name].filter(v => v !== input.value);
}
} else if (input.type === 'radio') {
if (input.checked) {
this.formData[input.name] = input.value;
}
} else {
this.formData[input.name] = input.value;
}
});
// Salvar no localStorage
localStorage.setItem('multiStepFormData', JSON.stringify(this.formData));
}
generateSummary() {
// Resumo dados pessoais
const personalSummary = document.getElementById('summary-personal');
personalSummary.innerHTML = `
<p><strong>Nome:</strong> ${this.formData.firstName} ${this.formData.lastName}</p>
<p><strong>E-mail:</strong> ${this.formData.email}</p>
<p><strong>Telefone:</strong> ${this.formData.phone}</p>
`;
// Resumo endereço
const addressSummary = document.getElementById('summary-address');
addressSummary.innerHTML = `
<p><strong>CEP:</strong> ${this.formData.cep}</p>
<p><strong>Endereço:</strong> ${this.formData.street}, ${this.formData.number}</p>
<p><strong>Cidade/Estado:</strong> ${this.formData.city}/${this.formData.state}</p>
`;
// Resumo preferências
const prefSummary = document.getElementById('summary-preferences');
const interests = Array.isArray(this.formData['interests[]']) ? this.formData['interests[]'].join(', ') : 'Nenhum';
prefSummary.innerHTML = `
<p><strong>Contato:</strong> ${this.formData.contactMethod}</p>
<p><strong>Interesses:</strong> ${interests}</p>
`;
}
async handleSubmit(e) {
e.preventDefault();
if (!this.validateCurrentStep()) {
return;
}
const submitBtn = document.getElementById('submit-btn');
const originalText = submitBtn.textContent;
submitBtn.textContent = '⏳ Enviando...';
submitBtn.disabled = true;
try {
// Simular envio
await new Promise(resolve => setTimeout(resolve, 2000));
submitBtn.textContent = '✅ Cadastro Realizado!';
// Limpar dados salvos
localStorage.removeItem('multiStepFormData');
// Redirecionar ou mostrar sucesso
setTimeout(() => {
alert('Cadastro realizado com sucesso!');
}, 1000);
} catch (error) {
submitBtn.textContent = '❌ Erro no envio';
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.disabled = false;
}, 3000);
}
}
isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
}
// Inicializar formulário
let multiStepForm;
document.addEventListener('DOMContentLoaded', () => {
multiStepForm = new MultiStepForm('multi-step-form');
});
// Funções globais para navegação
function nextStep() {
multiStepForm.nextStep();
}
function previousStep() {
multiStepForm.previousStep();
}
// Busca CEP (integração com API)
/*
💡 EXPLICAÇÃO: Integração com APIs em Formulários
A integração com APIs permite que os formulários sejam mais inteligentes e convenientes:
🌐 Por que integrar APIs:
• Preenchimento automático reduz erros em 60%
• Melhora a experiência do usuário significativamente
• Acelera o processo de preenchimento
• Reduz abandono de formulários em 25%
🔌 Tipos de integração comuns:
• CEP → Endereço completo (ViaCEP)
• CNPJ → Dados da empresa (ReceitaWS)
• Email → Verificação de domínio
• Telefone → Validação de formato por país
📊 Benefícios mensuráveis:
• +45% taxa de conclusão
• -60% tempo de preenchimento
• -35% erros de digitação
• +30% satisfação do usuário
⚙️ Implementação responsável:
• Sempre usar try/catch para errors
• Feedback visual durante carregamento
• Fallback para preenchimento manual
• Validação adicional dos dados recebidos
🚀 Dica profissional: APIs bem integradas transformam formulários "burros" em assistentes inteligentes!
*/
async function buscarCEP(cep) {
cep = cep.replace(/[^0-9]/g, '');
if (cep.length === 8) {
try {
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
const data = await response.json();
if (!data.erro) {
document.getElementById('street').value = data.logradouro;
document.getElementById('city').value = data.localidade;
document.getElementById('state').value = data.uf;
}
} catch (error) {
console.error('Erro ao buscar CEP:', error);
}
}
}
Para implementar os códigos desta aula, você tem 3 opções com estruturas de diretórios específicas para diferentes níveis de complexidade.
📂 meu-projeto-formularios/
└── 📄 formulario-avancado.html (todo código aqui)
Características:
📂 projeto-formularios/
├── 📄 index.html (estrutura HTML)
├── 📂 css/
│ └── 📄 formularios.css (todos os estilos)
├── 📂 js/
│ └── 📄 formularios.js (todo JavaScript)
└── 📂 assets/
└── 📂 images/
└── 📄 favicon.ico
Características:
Para OPÇÃO 1 (Arquivo único):
mkdir minha-aula-07
cd minha-aula-07
New-Item formulario-avancado-completo.html
Para OPÇÃO 2 (Separação básica):
mkdir projeto-formularios
cd projeto-formularios
mkdir css, js, assets, assets\images
New-Item index.html, css\formularios.css, js\formularios.js
🚀 Evolução Natural:
Aprenda → Arquivo único → Separação básica → Estrutura modular → Arquitetura profissional
Implemente a estrutura de arquivos para um projeto de formulários: