javascript-today

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