Form Validation
Client-side validation improves user experience by providing immediate feedback before form submission. Let’s explore both HTML5 validation and custom JavaScript validation.
HTML5 Validation
Modern browsers provide built-in validation:
Required Fields
<form>
<input type="text" name="username" required>
<input type="email" name="email" required>
<button type="submit">Submit</button>
</form>
Browser automatically shows error if empty when submitted.
Input Types
Different input types have automatic validation:
<!-- Email validation -->
<input type="email" name="email" required>
<!-- URL validation -->
<input type="url" name="website">
<!-- Number validation -->
<input type="number" name="age" min="18" max="100">
<!-- Date validation -->
<input type="date" name="birthdate">
<!-- Pattern matching -->
<input type="tel" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
placeholder="123-456-7890">
Validation Attributes
<input type="text"
name="username"
required
minlength="3"
maxlength="20"
pattern="[a-zA-Z0-9_]+"
placeholder="Username">
<input type="number"
name="quantity"
required
min="1"
max="100"
step="1">
<input type="email"
name="email"
required
placeholder="your@email.com">
Custom Validation Messages
const input = document.querySelector('#username');
input.addEventListener('invalid', (e) => {
e.preventDefault();
if (input.validity.valueMissing) {
input.setCustomValidity('Please enter a username');
} else if (input.validity.tooShort) {
input.setCustomValidity('Username must be at least 3 characters');
} else if (input.validity.patternMismatch) {
input.setCustomValidity('Username can only contain letters, numbers, and underscores');
}
});
// Clear custom message when user types
input.addEventListener('input', () => {
input.setCustomValidity('');
});
JavaScript Validation
Custom validation for complex requirements:
Basic Form Validation
<form id="registrationForm">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<span class="error" id="usernameError"></span>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email">
<span class="error" id="emailError"></span>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<span class="error" id="passwordError"></span>
</div>
<button type="submit">Register</button>
</form>
const form = document.querySelector('#registrationForm');
form.addEventListener('submit', (e) => {
e.preventDefault();
// Clear previous errors
clearErrors();
let isValid = true;
// Validate username
const username = document.querySelector('#username').value;
if (username.trim() === '') {
showError('username', 'Username is required');
isValid = false;
} else if (username.length < 3) {
showError('username', 'Username must be at least 3 characters');
isValid = false;
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
showError('username', 'Username can only contain letters, numbers, and underscores');
isValid = false;
}
// Validate email
const email = document.querySelector('#email').value;
if (email.trim() === '') {
showError('email', 'Email is required');
isValid = false;
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showError('email', 'Please enter a valid email address');
isValid = false;
}
// Validate password
const password = document.querySelector('#password').value;
if (password === '') {
showError('password', 'Password is required');
isValid = false;
} else if (password.length < 8) {
showError('password', 'Password must be at least 8 characters');
isValid = false;
} else if (!/[A-Z]/.test(password)) {
showError('password', 'Password must contain an uppercase letter');
isValid = false;
} else if (!/[0-9]/.test(password)) {
showError('password', 'Password must contain a number');
isValid = false;
}
if (isValid) {
console.log('Form is valid!');
// Submit form
}
});
function showError(fieldName, message) {
const errorElement = document.querySelector(`#${fieldName}Error`);
const inputElement = document.querySelector(`#${fieldName}`);
errorElement.textContent = message;
inputElement.classList.add('invalid');
}
function clearErrors() {
document.querySelectorAll('.error').forEach(error => {
error.textContent = '';
});
document.querySelectorAll('input').forEach(input => {
input.classList.remove('invalid');
});
}
Real-Time Validation
Validate as the user types:
const usernameInput = document.querySelector('#username');
const usernameError = document.querySelector('#usernameError');
usernameInput.addEventListener('input', () => {
const value = usernameInput.value;
if (value === '') {
usernameError.textContent = '';
usernameInput.classList.remove('valid', 'invalid');
} else if (value.length < 3) {
usernameError.textContent = 'Too short';
usernameInput.classList.add('invalid');
usernameInput.classList.remove('valid');
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
usernameError.textContent = 'Invalid characters';
usernameInput.classList.add('invalid');
usernameInput.classList.remove('valid');
} else {
usernameError.textContent = '';
usernameInput.classList.add('valid');
usernameInput.classList.remove('invalid');
}
});
Validation on Blur
Validate when field loses focus:
emailInput.addEventListener('blur', () => {
const value = emailInput.value.trim();
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
showError('email', 'Invalid email format');
}
});
emailInput.addEventListener('focus', () => {
clearError('email');
});
Validation Patterns
Email Validation
function validateEmail(email) {
// Basic pattern
const basicPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// More strict pattern
const strictPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return strictPattern.test(email);
}
Password Strength
function validatePassword(password) {
const errors = [];
if (password.length < 8) {
errors.push('At least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('At least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('At least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('At least one number');
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push('At least one special character (!@#$%^&*)');
}
return errors;
}
// Usage
const passwordInput = document.querySelector('#password');
const strengthIndicator = document.querySelector('#strength');
passwordInput.addEventListener('input', () => {
const errors = validatePassword(passwordInput.value);
if (errors.length === 0) {
strengthIndicator.textContent = 'Strong password ✓';
strengthIndicator.className = 'strength-strong';
} else if (errors.length <= 2) {
strengthIndicator.textContent = 'Moderate password';
strengthIndicator.className = 'strength-moderate';
} else {
strengthIndicator.textContent = 'Weak password';
strengthIndicator.className = 'strength-weak';
}
});
Phone Number
function validatePhone(phone) {
// US phone number: (123) 456-7890 or 123-456-7890
const usPattern = /^(\(\d{3}\)|\d{3})[-\s]?\d{3}[-\s]?\d{4}$/;
// Remove spaces and dashes for validation
const cleaned = phone.replace(/[\s-()]/g, '');
return cleaned.length === 10 && /^\d+$/.test(cleaned);
}
Credit Card
function validateCreditCard(cardNumber) {
// Remove spaces
const cleaned = cardNumber.replace(/\s/g, '');
// Check if only digits
if (!/^\d+$/.test(cleaned)) {
return false;
}
// Luhn algorithm
let sum = 0;
let isEven = false;
for (let i = cleaned.length - 1; i >= 0; i--) {
let digit = parseInt(cleaned[i]);
if (isEven) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
Complete Validation Example
<form id="signupForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username">
<span class="error" id="usernameError"></span>
<span class="hint">3-20 characters, letters, numbers, and underscores only</span>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email">
<span class="error" id="emailError"></span>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password">
<span class="error" id="passwordError"></span>
<div id="passwordStrength"></div>
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" name="confirmPassword">
<span class="error" id="confirmPasswordError"></span>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="terms" name="terms">
I agree to the terms and conditions
</label>
<span class="error" id="termsError"></span>
</div>
<button type="submit">Sign Up</button>
</form>
const form = document.querySelector('#signupForm');
// Validation rules
const validators = {
username: (value) => {
if (!value) return 'Username is required';
if (value.length < 3) return 'Username must be at least 3 characters';
if (value.length > 20) return 'Username must be less than 20 characters';
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
return 'Username can only contain letters, numbers, and underscores';
}
return null;
},
email: (value) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Please enter a valid email address';
}
return null;
},
password: (value) => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/[A-Z]/.test(value)) return 'Password must contain an uppercase letter';
if (!/[a-z]/.test(value)) return 'Password must contain a lowercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain a number';
return null;
},
confirmPassword: (value, formData) => {
if (!value) return 'Please confirm your password';
if (value !== formData.password) return 'Passwords do not match';
return null;
},
terms: (checked) => {
if (!checked) return 'You must accept the terms and conditions';
return null;
}
};
// Real-time validation
Object.keys(validators).forEach(fieldName => {
const field = document.querySelector(`#${fieldName}`);
if (!field) return;
field.addEventListener('blur', () => {
validateField(fieldName);
});
field.addEventListener('input', () => {
// Clear error on input
if (field.classList.contains('invalid')) {
validateField(fieldName);
}
});
});
// Form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
let isValid = true;
Object.keys(validators).forEach(fieldName => {
if (!validateField(fieldName)) {
isValid = false;
}
});
if (isValid) {
console.log('Form is valid! Submitting...');
const formData = new FormData(form);
console.log(Object.fromEntries(formData));
// Submit to server
// form.submit();
} else {
// Focus first invalid field
const firstInvalid = form.querySelector('.invalid');
if (firstInvalid) {
firstInvalid.focus();
}
}
});
function validateField(fieldName) {
const field = document.querySelector(`#${fieldName}`);
const errorElement = document.querySelector(`#${fieldName}Error`);
if (!field || !errorElement) return true;
// Get value
const value = field.type === 'checkbox' ? field.checked : field.value.trim();
// Get form data for fields that depend on other fields
const formData = {};
new FormData(form).forEach((value, key) => {
formData[key] = value;
});
// Validate
const error = validators[fieldName](value, formData);
if (error) {
errorElement.textContent = error;
field.classList.add('invalid');
field.classList.remove('valid');
return false;
} else {
errorElement.textContent = '';
field.classList.remove('invalid');
if (value) {
field.classList.add('valid');
}
return true;
}
}
Accessible Validation
Make validation accessible:
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
aria-describedby="emailError emailHint"
aria-invalid="false">
<span class="hint" id="emailHint">We'll never share your email</span>
<span class="error" id="emailError" role="alert"></span>
</div>
function showError(fieldName, message) {
const field = document.querySelector(`#${fieldName}`);
const errorElement = document.querySelector(`#${fieldName}Error`);
errorElement.textContent = message;
field.setAttribute('aria-invalid', 'true');
field.classList.add('invalid');
}
function clearError(fieldName) {
const field = document.querySelector(`#${fieldName}`);
const errorElement = document.querySelector(`#${fieldName}Error`);
errorElement.textContent = '';
field.setAttribute('aria-invalid', 'false');
field.classList.remove('invalid');
}
CSS for Validation States
/* Valid state */
input.valid {
border-color: #28a745;
}
input.valid:focus {
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
/* Invalid state */
input.invalid {
border-color: #dc3545;
}
input.invalid:focus {
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
/* Error messages */
.error {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
/* Hints */
.hint {
color: #6c757d;
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
/* Using :valid and :invalid pseudo-classes */
input:valid {
border-color: #28a745;
}
input:invalid:not(:placeholder-shown) {
border-color: #dc3545;
}
Best Practices
✅ DO:
- Provide clear, helpful error messages
- Validate on blur and on submit
- Show success indicators for valid fields
- Make validation accessible (ARIA attributes)
- Validate on both client and server
- Clear errors when user starts typing
❌ DON’T:
- Show errors before user interacts
- Use generic error messages (“Invalid input”)
- Rely only on client-side validation
- Validate on every keystroke (use debounce)
- Make error messages too technical
- Forget to focus first invalid field
Summary
| Validation Type | When to Use |
|---|---|
| HTML5 required | Simple required fields |
| HTML5 pattern | Regular expression validation |
| JavaScript blur | When field loses focus |
| JavaScript input | Real-time feedback (debounced) |
| JavaScript submit | Final validation before submission |
| Server-side | Always (security) |
Common Patterns:
- Email:
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ - Phone:
/^\d{3}-\d{3}-\d{4}$/ - Username:
/^[a-zA-Z0-9_]{3,20}$/ - Password: Length + uppercase + lowercase + number
Next Article: FormData API