Skip to main content

Course Progress

Loading...

Form Validation with JavaScript

Duration: 45 minutes
Module 1: DOM Manipulation

Learning Objectives

  • Understand HTML structure and semantics
  • Create well-formed HTML documents
  • Use HTML tags effectively
  • Build accessible web content

Why Form Validation Matters

Form validation is a critical aspect of web development that ensures users provide correctly formatted data before it's processed by the server. Proper validation improves user experience, data quality, and application security.

Form Validation Flow User Input Submit Validation Valid Process Data Invalid Show Error Success Message

Benefits of Client-Side Validation

  • Immediate Feedback: Users get instant feedback about their input without waiting for a server response
  • Better User Experience: Guides users to correct errors in real-time, reducing frustration
  • Reduced Server Load: Prevents invalid submissions from reaching the server
  • Bandwidth Savings: No need to send and receive data for invalid submissions

Client-Side vs. Server-Side Validation

Important: While client-side validation improves user experience, it should never be the only form of validation. Always implement server-side validation as well, as client-side validation can be bypassed by disabling JavaScript or using developer tools.

Client-Side Validation Server-Side Validation
Provides immediate feedback Ensures data integrity and security
Enhances user experience Cannot be bypassed by users
Reduces server load Protects against malicious attacks
Can be bypassed Required for true data validation

Form Validation as Security Guard

Think of form validation like a security check at an airport:

  • Client-side validation is like the initial ID check at the entrance - it's quick and catches obvious issues, but determined individuals could potentially bypass it.
  • Server-side validation is like the thorough security screening with X-ray machines and metal detectors - it's more comprehensive and cannot be bypassed.

Both layers are important: the first improves efficiency and experience, while the second ensures true security.

Approaches to Form Validation

There are several approaches to implementing client-side form validation with JavaScript:

Three Main Approaches

Built-in HTML5 Validation

Using HTML5 attributes like required, pattern, min, max, etc.

Pros: Simple to implement, built into the browser, no JavaScript required

Cons: Limited styling options, less control over error messages and validation behavior

JavaScript Validation

Custom validation logic using JavaScript to check form values, display errors, and manage form submission

Pros: Complete control over validation logic, error messages, and user experience

Cons: Requires more code, needs careful implementation to be accessible

Constraint Validation API

JavaScript API that leverages HTML5 validation but provides more control

Pros: Combines simplicity of HTML5 with power of JavaScript

Cons: Less browser support for older browsers, more complex than pure HTML5

Form Validation Approaches Form Validation HTML5 Validation JavaScript Validation Constraint API Required Type=email/etc Min/Max/Pattern Manual Validation Regular Expressions Custom Logic ValidityState setCustomValidity checkValidity HTML5: Simple, built-in JavaScript: Full control API: Best of both

HTML5 Built-in Validation

HTML5 introduced several attributes that enable browser-native form validation:

<form id="signup-form">
  <div class="form-group">
    <label for="username">Username:</label>
    <input 
      type="text" 
      id="username" 
      name="username" 
      required 
      minlength="3" 
      maxlength="20"
    >
  </div>
  
  <div class="form-group">
    <label for="email">Email:</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      required
    >
  </div>
  
  <div class="form-group">
    <label for="password">Password:</label>
    <input 
      type="password" 
      id="password" 
      name="password" 
      required 
      minlength="8"
      pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
      title="Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
    >
  </div>
  
  <div class="form-group">
    <label for="age">Age:</label>
    <input 
      type="number" 
      id="age" 
      name="age" 
      min="18" 
      max="120"
    >
  </div>
  
  <div class="form-group">
    <label for="website">Website:</label>
    <input 
      type="url" 
      id="website" 
      name="website"
    >
  </div>
  
  <button type="submit">Sign Up</button>
</form>

Common HTML5 Validation Attributes

Attribute Description Example
required Field must have a value <input required>
minlength / maxlength Minimum/maximum number of characters <input minlength="3" maxlength="20">
min / max Minimum/maximum value for numbers <input type="number" min="0" max="100">
pattern Regular expression pattern <input pattern="[A-Za-z0-9]+">
type Type of input (email, url, number, etc.) <input type="email">
step Incremental values for number inputs <input type="number" step="0.5">
title Tooltip explaining the expected input <input title="Enter alphabetic characters only">

Input Types with Built-in Validation

  • email - Validates email format
  • url - Ensures valid URL format
  • number - Accepts only numeric input
  • tel - For telephone numbers
  • date, time, datetime-local - For date/time values
  • color - Color input with built-in picker
  • range - Slider control for numbers within a range

Limitations of HTML5 Validation

  • Limited control over error messages
  • Difficult to customize validation appearance across browsers
  • Cannot implement complex validation rules
  • No control over when validation occurs
  • Limited cross-field validation (e.g., comparing password and confirm password)

Styling HTML5 Validation Messages

/* Basic styling for valid/invalid inputs */
input:valid {
  border: 2px solid green;
}

input:invalid {
  border: 2px solid red;
}

/* Only show invalid styling after user interaction */
input:not(:focus):invalid {
  border: 2px solid red;
  background-color: #fdd;
}

/* Custom styling for validation message tooltips (limited browser support) */
::-webkit-validation-bubble-message {
  background: #d32f2f;
  color: white;
  padding: 1em;
}

/* Disabling browser default validation styles */
input {
  box-shadow: none !important;
}

Browser Support Note

While most modern browsers support HTML5 form validation, the visual appearance of validation messages varies significantly between browsers and is difficult to style consistently. For fully customized validation messages, JavaScript validation is recommended.

Custom JavaScript Validation

JavaScript validation gives you complete control over form validation, allowing custom validation rules, error messages, and user experience.

Basic JavaScript Validation Pattern

<form id="signup-form" novalidate>
  <div class="form-group">
    <label for="username">Username:</label>
    <input type="text" id="username" name="username">
    <span class="error" id="username-error"></span>
  </div>
  
  <div class="form-group">
    <label for="email">Email:</label>
    <input type="email" id="email" name="email">
    <span class="error" id="email-error"></span>
  </div>
  
  <div class="form-group">
    <label for="password">Password:</label>
    <input type="password" id="password" name="password">
    <span class="error" id="password-error"></span>
  </div>
  
  <div class="form-group">
    <label for="confirm-password">Confirm Password:</label>
    <input type="password" id="confirm-password" name="confirm-password">
    <span class="error" id="confirm-password-error"></span>
  </div>
  
  <button type="submit">Sign Up</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('signup-form');
  
  // Form submission validation
  form.addEventListener('submit', function(event) {
    // Prevent the form from submitting automatically
    event.preventDefault();
    
    // Get form fields
    const username = document.getElementById('username');
    const email = document.getElementById('email');
    const password = document.getElementById('password');
    const confirmPassword = document.getElementById('confirm-password');
    
    // Get error elements
    const usernameError = document.getElementById('username-error');
    const emailError = document.getElementById('email-error');
    const passwordError = document.getElementById('password-error');
    const confirmPasswordError = document.getElementById('confirm-password-error');
    
    // Reset previous error messages
    usernameError.textContent = '';
    emailError.textContent = '';
    passwordError.textContent = '';
    confirmPasswordError.textContent = '';
    
    // Flag to track validation status
    let isValid = true;
    
    // Validate username
    if (username.value.trim() === '') {
      usernameError.textContent = 'Username is required';
      isValid = false;
    } else if (username.value.length < 3) {
      usernameError.textContent = 'Username must be at least 3 characters';
      isValid = false;
    }
    
    // Validate email
    if (email.value.trim() === '') {
      emailError.textContent = 'Email is required';
      isValid = false;
    } else if (!isValidEmail(email.value)) {
      emailError.textContent = 'Please enter a valid email address';
      isValid = false;
    }
    
    // Validate password
    if (password.value === '') {
      passwordError.textContent = 'Password is required';
      isValid = false;
    } else if (password.value.length < 8) {
      passwordError.textContent = 'Password must be at least 8 characters';
      isValid = false;
    } else if (!isStrongPassword(password.value)) {
      passwordError.textContent = 'Password must include uppercase, lowercase, number, and special character';
      isValid = false;
    }
    
    // Validate password confirmation
    if (confirmPassword.value === '') {
      confirmPasswordError.textContent = 'Please confirm your password';
      isValid = false;
    } else if (confirmPassword.value !== password.value) {
      confirmPasswordError.textContent = 'Passwords do not match';
      isValid = false;
    }
    
    // If all validations pass, submit the form
    if (isValid) {
      form.submit();
    }
  });
  
  // Helper function to validate email format
  function isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  // Helper function to check password strength
  function isStrongPassword(password) {
    // Password must contain at least one uppercase letter, one lowercase letter,
    // one number, and one special character
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    return passwordRegex.test(password);
  }
});
</script>

The novalidate Attribute

Notice the novalidate attribute on the form element. This disables the browser's built-in validation so you can implement your own validation logic.

Real-Time Validation

For a better user experience, you can validate fields as users type or when they leave a field (blur event):

// Add real-time validation to the previous example
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('signup-form');
  const username = document.getElementById('username');
  const email = document.getElementById('email');
  const password = document.getElementById('password');
  const confirmPassword = document.getElementById('confirm-password');
  
  const usernameError = document.getElementById('username-error');
  const emailError = document.getElementById('email-error');
  const passwordError = document.getElementById('password-error');
  const confirmPasswordError = document.getElementById('confirm-password-error');
  
  // Real-time username validation
  username.addEventListener('input', function() {
    if (username.value.trim() === '') {
      usernameError.textContent = 'Username is required';
      username.classList.add('invalid');
    } else if (username.value.length < 3) {
      usernameError.textContent = 'Username must be at least 3 characters';
      username.classList.add('invalid');
    } else {
      usernameError.textContent = '';
      username.classList.remove('invalid');
      username.classList.add('valid');
    }
  });
  
  // Validate email when user leaves the field
  email.addEventListener('blur', function() {
    if (email.value.trim() === '') {
      emailError.textContent = 'Email is required';
      email.classList.add('invalid');
    } else if (!isValidEmail(email.value)) {
      emailError.textContent = 'Please enter a valid email address';
      email.classList.add('invalid');
    } else {
      emailError.textContent = '';
      email.classList.remove('invalid');
      email.classList.add('valid');
    }
  });
  
  // Real-time password strength validation
  password.addEventListener('input', function() {
    if (password.value === '') {
      passwordError.textContent = 'Password is required';
      password.classList.add('invalid');
    } else if (password.value.length < 8) {
      passwordError.textContent = 'Password must be at least 8 characters';
      password.classList.add('invalid');
    } else if (!isStrongPassword(password.value)) {
      passwordError.textContent = 'Password must include uppercase, lowercase, number, and special character';
      password.classList.add('invalid');
    } else {
      passwordError.textContent = '';
      password.classList.remove('invalid');
      password.classList.add('valid');
    }
    
    // Also validate confirm password if it has a value
    if (confirmPassword.value !== '') {
      if (confirmPassword.value !== password.value) {
        confirmPasswordError.textContent = 'Passwords do not match';
        confirmPassword.classList.add('invalid');
      } else {
        confirmPasswordError.textContent = '';
        confirmPassword.classList.remove('invalid');
        confirmPassword.classList.add('valid');
      }
    }
  });
  
  // Validate confirm password field
  confirmPassword.addEventListener('input', function() {
    if (confirmPassword.value === '') {
      confirmPasswordError.textContent = 'Please confirm your password';
      confirmPassword.classList.add('invalid');
    } else if (confirmPassword.value !== password.value) {
      confirmPasswordError.textContent = 'Passwords do not match';
      confirmPassword.classList.add('invalid');
    } else {
      confirmPasswordError.textContent = '';
      confirmPassword.classList.remove('invalid');
      confirmPassword.classList.add('valid');
    }
  });
  
  // Form submission validation (same as before)
  form.addEventListener('submit', function(event) {
    // Validation code from the previous example...
  });
});

CSS for Custom Validation Styling

/* Basic form styling */
.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  transition: border-color 0.3s;
}

/* Validation states */
.form-group input.valid {
  border-color: #28a745;
  background-image: url('check-icon.svg');
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 16px;
}

.form-group input.invalid {
  border-color: #dc3545;
  background-color: #fff8f8;
}

/* Error messages */
.error {
  display: block;
  color: #dc3545;
  font-size: 12px;
  margin-top: 5px;
  min-height: 18px; /* Prevent layout shifts */
}

/* Password strength meter (optional) */
.password-strength-meter {
  height: 5px;
  width: 100%;
  background-color: #f3f3f3;
  margin-top: 5px;
  border-radius: 3px;
  overflow: hidden;
}

.password-strength-meter div {
  height: 100%;
  width: 0;
  transition: width 0.3s, background-color 0.3s;
}

.strength-weak {
  width: 25% !important;
  background-color: #dc3545;
}

.strength-medium {
  width: 50% !important;
  background-color: #ffc107;
}

.strength-strong {
  width: 75% !important;
  background-color: #17a2b8;
}

.strength-very-strong {
  width: 100% !important;
  background-color: #28a745;
}

Constraint Validation API

The Constraint Validation API provides a bridge between HTML5 validation and JavaScript, offering more control while leveraging browser capabilities.

Key Features of the Constraint Validation API

  • Access to detailed validation information through the ValidityState object
  • Ability to trigger validation programmatically
  • Custom error messages with setCustomValidity()
  • Control over when validation occurs
<form id="payment-form">
  <div class="form-group">
    <label for="card-number">Credit Card Number:</label>
    <input 
      type="text" 
      id="card-number" 
      name="card-number" 
      required 
      pattern="[0-9]{16}"
    >
    <span class="error" id="card-number-error"></span>
  </div>
  
  <div class="form-group">
    <label for="expiry-date">Expiry Date (MM/YY):</label>
    <input 
      type="text" 
      id="expiry-date" 
      name="expiry-date" 
      required 
      pattern="(0[1-9]|1[0-2])\/([0-9]{2})"
    >
    <span class="error" id="expiry-date-error"></span>
  </div>
  
  <div class="form-group">
    <label for="cvv">CVV:</label>
    <input 
      type="text" 
      id="cvv" 
      name="cvv" 
      required 
      pattern="[0-9]{3,4}"
    >
    <span class="error" id="cvv-error"></span>
  </div>
  
  <button type="submit">Submit Payment</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('payment-form');
  const cardInput = document.getElementById('card-number');
  const expiryInput = document.getElementById('expiry-date');
  const cvvInput = document.getElementById('cvv');
  
  const cardError = document.getElementById('card-number-error');
  const expiryError = document.getElementById('expiry-date-error');
  const cvvError = document.getElementById('cvv-error');
  
  // Custom validation for card number
  cardInput.addEventListener('input', function() {
    // Clear previous custom validity
    cardInput.setCustomValidity('');
    
    const cardNumber = cardInput.value.replace(/\s/g, '');
    
    // Check if it contains only digits
    if (!/^\d+$/.test(cardNumber)) {
      cardInput.setCustomValidity('Card number must contain only digits');
    } 
    // Check if it's the right length
    else if (cardNumber.length !== 16) {
      cardInput.setCustomValidity('Card number must be 16 digits');
    }
    // Implement Luhn algorithm (credit card checksum)
    else if (!isValidCreditCard(cardNumber)) {
      cardInput.setCustomValidity('Invalid card number');
    }
    
    // Update the error message
    cardError.textContent = cardInput.validationMessage;
  });
  
  // Custom validation for expiry date
  expiryInput.addEventListener('input', function() {
    expiryInput.setCustomValidity('');
    
    if (expiryInput.validity.patternMismatch) {
      expiryInput.setCustomValidity('Please enter a valid expiry date in MM/YY format');
    } else {
      // Check if the card is expired
      const [month, year] = expiryInput.value.split('/');
      const expiryDate = new Date(2000 + parseInt(year), parseInt(month) - 1, 1);
      const today = new Date();
      
      if (expiryDate < today) {
        expiryInput.setCustomValidity('Card has expired');
      }
    }
    
    // Update the error message
    expiryError.textContent = expiryInput.validationMessage;
  });
  
  // Show validation state on blur
  cvvInput.addEventListener('blur', function() {
    cvvError.textContent = cvvInput.validationMessage;
    
    // Check validation state properties
    if (cvvInput.validity.valueMissing) {
      cvvError.textContent = 'CVV is required';
    } else if (cvvInput.validity.patternMismatch) {
      cvvError.textContent = 'CVV must be 3 or 4 digits';
    }
  });
  
  // Validate the entire form on submit
  form.addEventListener('submit', function(event) {
    // Check if the form is valid using the checkValidity method
    if (!form.checkValidity()) {
      event.preventDefault();
      
      // Show error messages for all invalid fields
      if (!cardInput.validity.valid) {
        cardError.textContent = cardInput.validationMessage;
      }
      
      if (!expiryInput.validity.valid) {
        expiryError.textContent = expiryInput.validationMessage;
      }
      
      if (!cvvInput.validity.valid) {
        cvvError.textContent = cvvInput.validationMessage;
      }
    }
  });
  
  // Helper function for Luhn algorithm (credit card validation)
  function isValidCreditCard(number) {
    // Remove any spaces or dashes
    number = number.replace(/\D/g, '');
    
    // Check if the number is a valid length
    if (number.length !== 16) {
      return false;
    }
    
    // Luhn algorithm
    let sum = 0;
    let shouldDouble = false;
    
    // Loop through values starting from the rightmost digit
    for (let i = number.length - 1; i >= 0; i--) {
      let digit = parseInt(number.charAt(i));
      
      if (shouldDouble) {
        digit *= 2;
        if (digit > 9) {
          digit -= 9;
        }
      }
      
      sum += digit;
      shouldDouble = !shouldDouble;
    }
    
    return (sum % 10) === 0;
  }
});
</script>

ValidityState Object Properties

Property Description Related Attribute
validity.valueMissing Element has no value but is required required
validity.typeMismatch Value doesn't match expected type type="email", type="url"
validity.patternMismatch Value doesn't match pattern pattern
validity.tooLong Value exceeds maxlength maxlength
validity.tooShort Value less than minlength minlength
validity.rangeUnderflow Value less than min min
validity.rangeOverflow Value greater than max max
validity.stepMismatch Value doesn't align with step value step
validity.badInput Browser can't convert input to value N/A
validity.customError Custom validation error set with setCustomValidity N/A
validity.valid Element meets all validation constraints N/A

Key Constraint Validation API Methods

  • element.checkValidity() - Returns true if the element's value satisfies all constraints
  • element.reportValidity() - Checks validity and shows browser validation message if invalid
  • element.setCustomValidity(message) - Sets a custom validation message (empty string = valid)
  • form.checkValidity() - Returns true if all form elements are valid

Advanced Validation Techniques

Password Strength Meter

<div class="form-group">
  <label for="password">Password:</label>
  <input type="password" id="password" name="password">
  <div class="password-strength-meter">
    <div id="strength-meter-bar"></div>
  </div>
  <span id="strength-text">Password Strength</span>
  <span class="error" id="password-error"></span>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const passwordInput = document.getElementById('password');
  const strengthMeter = document.getElementById('strength-meter-bar');
  const strengthText = document.getElementById('strength-text');
  
  passwordInput.addEventListener('input', function() {
    const password = passwordInput.value;
    const strength = calculatePasswordStrength(password);
    
    // Update strength meter
    strengthMeter.className = '';
    if (strength === 0) {
      strengthMeter.style.width = '0';
      strengthText.textContent = 'Password Strength';
    } else if (strength < 40) {
      strengthMeter.classList.add('strength-weak');
      strengthText.textContent = 'Weak';
    } else if (strength < 80) {
      strengthMeter.classList.add('strength-medium');
      strengthText.textContent = 'Medium';
    } else if (strength < 100) {
      strengthMeter.classList.add('strength-strong');
      strengthText.textContent = 'Strong';
    } else {
      strengthMeter.classList.add('strength-very-strong');
      strengthText.textContent = 'Very Strong';
    }
  });
  
  function calculatePasswordStrength(password) {
    // Start with a base score
    let score = 0;
    
    // If password is empty, return 0
    if (password.length === 0) return 0;
    
    // Score for length
    score += Math.min(password.length * 4, 40);
    
    // Score for character variation
    const variations = {
      digits: /\d/.test(password),
      lower: /[a-z]/.test(password),
      upper: /[A-Z]/.test(password),
      nonWords: /\W/.test(password)
    };
    
    let variationCount = 0;
    for (const check in variations) {
      variationCount += variations[check] ? 1 : 0;
    }
    
    score += variationCount * 10;
    
    // Bonus for mixing character types
    if (variationCount > 2) {
      score += 10;
    }
    
    // Penalize for repeating patterns
    if (/(.)\1\1/.test(password)) {
      score -= 20;
    }
    
    // Penalize for sequential characters
    if (/(?:012|123|234|345|456|567|678|789|abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i.test(password)) {
      score -= 15;
    }
    
    // Cap the score at 100
    return Math.max(0, Math.min(100, score));
  }
});
</script>

Asynchronous Validation (e.g., Username Availability)

<div class="form-group">
  <label for="username">Username:</label>
  <input type="text" id="username" name="username">
  <span class="status" id="username-status"></span>
  <span class="error" id="username-error"></span>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const usernameInput = document.getElementById('username');
  const usernameStatus = document.getElementById('username-status');
  const usernameError = document.getElementById('username-error');
  
  let checkUsernameTimeout;
  
  usernameInput.addEventListener('input', function() {
    const username = usernameInput.value.trim();
    
    // Basic validation
    if (username.length === 0) {
      usernameStatus.textContent = '';
      usernameError.textContent = 'Username is required';
      return;
    } else if (username.length < 3) {
      usernameStatus.textContent = '';
      usernameError.textContent = 'Username must be at least 3 characters';
      return;
    } else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
      usernameStatus.textContent = '';
      usernameError.textContent = 'Username can only contain letters, numbers, and underscores';
      return;
    }
    
    // Clear previous errors since basic validation passed
    usernameError.textContent = '';
    
    // Clear previous timeout
    clearTimeout(checkUsernameTimeout);
    
    // Show checking status
    usernameStatus.textContent = 'Checking availability...';
    
    // Set a timeout to avoid too many requests while typing
    checkUsernameTimeout = setTimeout(function() {
      // In a real app, this would be an API call
      checkUsernameAvailability(username)
        .then(isAvailable => {
          if (isAvailable) {
            usernameStatus.textContent = '✓ Username is available';
            usernameStatus.className = 'status available';
            usernameInput.setCustomValidity('');
          } else {
            usernameStatus.textContent = '✗ Username is already taken';
            usernameStatus.className = 'status unavailable';
            usernameInput.setCustomValidity('This username is already taken');
          }
        })
        .catch(error => {
          console.error('Error checking username:', error);
          usernameStatus.textContent = 'Could not check availability';
        });
    }, 500); // 500ms delay
  });
  
  // Simulate an API call to check username availability
  // In a real app, this would be a fetch request to your server
  function checkUsernameAvailability(username) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // Simulate unavailable usernames (in real app, this would be a server check)
        const unavailableUsernames = ['admin', 'user', 'moderator', 'test'];
        resolve(!unavailableUsernames.includes(username.toLowerCase()));
      }, 1000); // 1s delay to simulate network request
    });
  }
});
</script>

Dynamic Form Validation (Adding and Removing Fields)

<form id="survey-form">
  <div id="questions-container">
    <div class="question-block" data-question-id="1">
      <div class="form-group">
        <label for="question-1">Question 1:</label>
        <input type="text" id="question-1" name="question-1" required>
        <span class="error" id="question-1-error"></span>
      </div>
      <button type="button" class="remove-question" disabled>Remove</button>
    </div>
  </div>
  
  <button type="button" id="add-question">Add Another Question</button>
  <button type="submit">Submit Survey</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('survey-form');
  const questionsContainer = document.getElementById('questions-container');
  const addQuestionButton = document.getElementById('add-question');
  
  let questionCount = 1;
  
  // Add a new question
  addQuestionButton.addEventListener('click', function() {
    questionCount++;
    
    const questionBlock = document.createElement('div');
    questionBlock.className = 'question-block';
    questionBlock.dataset.questionId = questionCount;
    
    const formGroup = document.createElement('div');
    formGroup.className = 'form-group';
    
    const label = document.createElement('label');
    label.htmlFor = `question-${questionCount}`;
    label.textContent = `Question ${questionCount}:`;
    
    const input = document.createElement('input');
    input.type = 'text';
    input.id = `question-${questionCount}`;
    input.name = `question-${questionCount}`;
    input.required = true;
    
    const errorSpan = document.createElement('span');
    errorSpan.className = 'error';
    errorSpan.id = `question-${questionCount}-error`;
    
    const removeButton = document.createElement('button');
    removeButton.type = 'button';
    removeButton.className = 'remove-question';
    removeButton.textContent = 'Remove';
    
    // Add remove functionality
    removeButton.addEventListener('click', function() {
      questionBlock.remove();
      
      // Re-check if remove buttons should be disabled
      updateRemoveButtons();
    });
    
    // Assemble the question block
    formGroup.appendChild(label);
    formGroup.appendChild(input);
    formGroup.appendChild(errorSpan);
    questionBlock.appendChild(formGroup);
    questionBlock.appendChild(removeButton);
    
    // Add to the container
    questionsContainer.appendChild(questionBlock);
    
    // Update remove buttons state
    updateRemoveButtons();
    
    // Add input validation
    setupValidation(input, errorSpan);
  });
  
  // Setup validation for the initial question
  const initialInput = document.getElementById('question-1');
  const initialError = document.getElementById('question-1-error');
  setupValidation(initialInput, initialError);
  
  // Function to update remove buttons state
  function updateRemoveButtons() {
    const removeButtons = document.querySelectorAll('.remove-question');
    
    // If there's only one question, disable the remove button
    if (removeButtons.length === 1) {
      removeButtons[0].disabled = true;
    } else {
      // Enable all remove buttons
      removeButtons.forEach(button => {
        button.disabled = false;
      });
    }
  }
  
  // Function to set up validation for an input
  function setupValidation(input, errorSpan) {
    input.addEventListener('input', function() {
      if (input.validity.valueMissing) {
        errorSpan.textContent = 'This question is required';
      } else {
        errorSpan.textContent = '';
      }
    });
    
    input.addEventListener('blur', function() {
      if (input.validity.valueMissing) {
        errorSpan.textContent = 'This question is required';
        input.classList.add('invalid');
      } else {
        errorSpan.textContent = '';
        input.classList.remove('invalid');
        input.classList.add('valid');
      }
    });
  }
  
  // Form submission
  form.addEventListener('submit', function(event) {
    event.preventDefault();
    
    let isValid = true;
    
    // Validate all inputs
    const allInputs = form.querySelectorAll('input[required]');
    allInputs.forEach(input => {
      if (!input.validity.valid) {
        isValid = false;
        const errorSpan = document.getElementById(`${input.id}-error`);
        errorSpan.textContent = 'This question is required';
        input.classList.add('invalid');
      }
    });
    
    if (isValid) {
      // In a real app, submit the form data
      alert('Form submitted successfully');
      console.log('Form data:', new FormData(form));
    }
  });
});
</script>

Accessibility Considerations

Creating accessible form validation is essential for users with disabilities. Here are key considerations:

Accessible Error Messages

<div class="form-group">
  <label for="email">Email:</label>
  <input 
    type="email" 
    id="email" 
    name="email" 
    aria-describedby="email-error email-hint" 
    required
  >
  <span id="email-hint" class="hint">Enter your email address</span>
  <span id="email-error" class="error" role="alert" aria-live="assertive"></span>
</div>

<script>
  const emailInput = document.getElementById('email');
  const emailError = document.getElementById('email-error');
  
  emailInput.addEventListener('input', function() {
    if (emailInput.validity.valid) {
      emailError.textContent = '';
      emailInput.setAttribute('aria-invalid', 'false');
    } else {
      if (emailInput.validity.valueMissing) {
        emailError.textContent = 'Email is required';
      } else if (emailInput.validity.typeMismatch) {
        emailError.textContent = 'Please enter a valid email address';
      }
      emailInput.setAttribute('aria-invalid', 'true');
    }
  });
</script>

Key Accessibility Attributes

Attribute Purpose Example
aria-describedby Links an element to its description aria-describedby="email-error"
aria-invalid Indicates validation state aria-invalid="true"
aria-live Announces dynamic changes aria-live="assertive"
role="alert" Marks content as important/time-sensitive role="alert"

Accessibility Best Practices

  • Use proper labels for all form controls
  • Provide clear, descriptive error messages
  • Make validation errors easy to understand and fix
  • Use color plus other indicators (icons, text) for error states
  • Ensure sufficient color contrast for all text
  • Allow keyboard navigation throughout the form
  • Give focused elements a visible focus indicator
  • Group related form controls with fieldset and legend
  • Announce errors appropriately to screen readers
  • Test with screen readers and keyboard-only navigation

Practical Complete Examples

Registration Form with Multiple Validation Types

<!-- Complete Registration Form Example -->
<form id="registration-form" novalidate>
  <h2>Create Account</h2>
  
  <div class="form-group">
    <label for="fullname">Full Name</label>
    <input 
      type="text" 
      id="fullname" 
      name="fullname" 
      required 
      aria-describedby="fullname-error"
    >
    <span class="error" id="fullname-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group">
    <label for="email">Email Address</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      required 
      aria-describedby="email-error"
    >
    <span class="error" id="email-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group">
    <label for="username">Username</label>
    <input 
      type="text" 
      id="username" 
      name="username" 
      required 
      minlength="3" 
      maxlength="20" 
      pattern="^[a-zA-Z0-9_]+$"
      aria-describedby="username-hint username-error"
    >
    <span class="hint" id="username-hint">3-20 characters, letters, numbers, and underscores only</span>
    <span class="error" id="username-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group">
    <label for="password">Password</label>
    <input 
      type="password" 
      id="password" 
      name="password" 
      required 
      minlength="8"
      aria-describedby="password-hint password-error"
    >
    <span class="hint" id="password-hint">At least 8 characters with uppercase, lowercase, number, and special character</span>
    <div class="password-strength-meter">
      <div id="strength-meter-bar"></div>
    </div>
    <span id="strength-text"></span>
    <span class="error" id="password-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group">
    <label for="confirm-password">Confirm Password</label>
    <input 
      type="password" 
      id="confirm-password" 
      name="confirm-password" 
      required
      aria-describedby="confirm-password-error"
    >
    <span class="error" id="confirm-password-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group">
    <label for="dob">Date of Birth</label>
    <input 
      type="date" 
      id="dob" 
      name="dob" 
      required
      aria-describedby="dob-error"
    >
    <span class="error" id="dob-error" role="alert" aria-live="polite"></span>
  </div>
  
  <div class="form-group checkbox-group">
    <input 
      type="checkbox" 
      id="terms" 
      name="terms" 
      required
      aria-describedby="terms-error"
    >
    <label for="terms">I agree to the <a href="#">Terms and Conditions</a></label>
    <span class="error" id="terms-error" role="alert" aria-live="polite"></span>
  </div>
  
  <button type="submit" class="submit-button">Create Account</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('registration-form');
  
  // Get all form elements
  const fullnameInput = document.getElementById('fullname');
  const emailInput = document.getElementById('email');
  const usernameInput = document.getElementById('username');
  const passwordInput = document.getElementById('password');
  const confirmPasswordInput = document.getElementById('confirm-password');
  const dobInput = document.getElementById('dob');
  const termsCheckbox = document.getElementById('terms');
  
  // Get all error elements
  const fullnameError = document.getElementById('fullname-error');
  const emailError = document.getElementById('email-error');
  const usernameError = document.getElementById('username-error');
  const passwordError = document.getElementById('password-error');
  const confirmPasswordError = document.getElementById('confirm-password-error');
  const dobError = document.getElementById('dob-error');
  const termsError = document.getElementById('terms-error');
  
  // Get password strength elements
  const strengthMeter = document.getElementById('strength-meter-bar');
  const strengthText = document.getElementById('strength-text');
  
  // Fullname validation
  fullnameInput.addEventListener('input', function() {
    if (fullnameInput.validity.valueMissing) {
      showError(fullnameInput, fullnameError, 'Please enter your full name');
    } else if (fullnameInput.value.trim().split(' ').length < 2) {
      showError(fullnameInput, fullnameError, 'Please enter your first and last name');
    } else {
      clearError(fullnameInput, fullnameError);
    }
  });
  
  // Email validation
  emailInput.addEventListener('input', function() {
    if (emailInput.validity.valueMissing) {
      showError(emailInput, emailError, 'Please enter your email address');
    } else if (emailInput.validity.typeMismatch) {
      showError(emailInput, emailError, 'Please enter a valid email address');
    } else {
      clearError(emailInput, emailError);
    }
  });
  
  // Username validation
  usernameInput.addEventListener('input', function() {
    if (usernameInput.validity.valueMissing) {
      showError(usernameInput, usernameError, 'Please enter a username');
    } else if (usernameInput.validity.tooShort) {
      showError(usernameInput, usernameError, 'Username must be at least 3 characters');
    } else if (usernameInput.validity.tooLong) {
      showError(usernameInput, usernameError, 'Username must be no more than 20 characters');
    } else if (usernameInput.validity.patternMismatch) {
      showError(usernameInput, usernameError, 'Username can only contain letters, numbers, and underscores');
    } else {
      clearError(usernameInput, usernameError);
    }
  });
  
  // Password validation and strength meter
  passwordInput.addEventListener('input', function() {
    // Update password strength meter
    const password = passwordInput.value;
    const strength = calculatePasswordStrength(password);
    
    // Update strength meter
    strengthMeter.className = '';
    if (strength === 0) {
      strengthMeter.style.width = '0';
      strengthText.textContent = '';
    } else if (strength < 40) {
      strengthMeter.classList.add('strength-weak');
      strengthText.textContent = 'Weak';
    } else if (strength < 80) {
      strengthMeter.classList.add('strength-medium');
      strengthText.textContent = 'Medium';
    } else {
      strengthMeter.classList.add('strength-strong');
      strengthText.textContent = 'Strong';
    }
    
    // Validate password
    if (passwordInput.validity.valueMissing) {
      showError(passwordInput, passwordError, 'Please enter a password');
    } else if (passwordInput.validity.tooShort) {
      showError(passwordInput, passwordError, 'Password must be at least 8 characters');
    } else if (!/[A-Z]/.test(password)) {
      showError(passwordInput, passwordError, 'Password must include at least one uppercase letter');
    } else if (!/[a-z]/.test(password)) {
      showError(passwordInput, passwordError, 'Password must include at least one lowercase letter');
    } else if (!/[0-9]/.test(password)) {
      showError(passwordInput, passwordError, 'Password must include at least one number');
    } else if (!/[^A-Za-z0-9]/.test(password)) {
      showError(passwordInput, passwordError, 'Password must include at least one special character');
    } else {
      clearError(passwordInput, passwordError);
    }
    
    // Also validate confirm password if it has a value
    if (confirmPasswordInput.value) {
      validatePasswordMatch();
    }
  });
  
  // Confirm password validation
  confirmPasswordInput.addEventListener('input', validatePasswordMatch);
  
  function validatePasswordMatch() {
    if (confirmPasswordInput.validity.valueMissing) {
      showError(confirmPasswordInput, confirmPasswordError, 'Please confirm your password');
    } else if (confirmPasswordInput.value !== passwordInput.value) {
      showError(confirmPasswordInput, confirmPasswordError, 'Passwords do not match');
    } else {
      clearError(confirmPasswordInput, confirmPasswordError);
    }
  }
  
  // Date of birth validation
  dobInput.addEventListener('input', function() {
    if (dobInput.validity.valueMissing) {
      showError(dobInput, dobError, 'Please enter your date of birth');
    } else {
      // Check if user is at least 18 years old
      const dobDate = new Date(dobInput.value);
      const today = new Date();
      const eighteenYearsAgo = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
      
      if (dobDate > eighteenYearsAgo) {
        showError(dobInput, dobError, 'You must be at least 18 years old to register');
      } else {
        clearError(dobInput, dobError);
      }
    }
  });
  
  // Terms checkbox validation
  termsCheckbox.addEventListener('change', function() {
    if (!termsCheckbox.checked) {
      showError(termsCheckbox, termsError, 'You must agree to the terms and conditions');
    } else {
      clearError(termsCheckbox, termsError);
    }
  });
  
  // Form submission
  form.addEventListener('submit', function(event) {
    // Prevent default form submission
    event.preventDefault();
    
    // Trigger validation for all fields
    fullnameInput.dispatchEvent(new Event('input'));
    emailInput.dispatchEvent(new Event('input'));
    usernameInput.dispatchEvent(new Event('input'));
    passwordInput.dispatchEvent(new Event('input'));
    confirmPasswordInput.dispatchEvent(new Event('input'));
    dobInput.dispatchEvent(new Event('input'));
    termsCheckbox.dispatchEvent(new Event('change'));
    
    // Check if the form is valid
    if (form.checkValidity() && 
        !fullnameError.textContent && 
        !emailError.textContent && 
        !usernameError.textContent && 
        !passwordError.textContent && 
        !confirmPasswordError.textContent && 
        !dobError.textContent && 
        !termsError.textContent) {
      
      // Form is valid - would normally submit to server
      console.log('Form submitted successfully!');
      alert('Registration successful!');
      
      // In a real application, you would submit the form data to your server here
      // form.submit();
    }
  });
  
  // Helper function to show error message
  function showError(input, errorElement, message) {
    errorElement.textContent = message;
    input.setAttribute('aria-invalid', 'true');
    input.classList.add('invalid');
    input.classList.remove('valid');
  }
  
  // Helper function to clear error message
  function clearError(input, errorElement) {
    errorElement.textContent = '';
    input.setAttribute('aria-invalid', 'false');
    input.classList.remove('invalid');
    input.classList.add('valid');
  }
  
  // Calculate password strength
  function calculatePasswordStrength(password) {
    // Start with a base score
    let score = 0;
    
    // If password is empty, return 0
    if (password.length === 0) return 0;
    
    // Score for length
    score += Math.min(password.length * 4, 40);
    
    // Score for character variation
    const variations = {
      digits: /\d/.test(password),
      lower: /[a-z]/.test(password),
      upper: /[A-Z]/.test(password),
      nonWords: /\W/.test(password)
    };
    
    let variationCount = 0;
    for (const check in variations) {
      variationCount += variations[check] ? 1 : 0;
    }
    
    score += variationCount * 10;
    
    // Bonus for mixing character types
    if (variationCount > 2) {
      score += 10;
    }
    
    // Penalize for repeating patterns
    if (/(.)\1\1/.test(password)) {
      score -= 20;
    }
    
    // Penalize for sequential characters
    if (/(?:012|123|234|345|456|567|678|789|abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i.test(password)) {
      score -= 15;
    }
    
    // Cap the score at 100
    return Math.max(0, Math.min(100, score));
  }
});
</script>

Multi-Step Form with Progressive Validation

<!-- Multi-Step Form HTML -->
<form id="multi-step-form" novalidate>
  <div class="form-progress">
    <div class="progress-step active" data-step="1">Personal Info</div>
    <div class="progress-step" data-step="2">Contact Details</div>
    <div class="progress-step" data-step="3">Account Setup</div>
    <div class="progress-step" data-step="4">Confirmation</div>
  </div>
  
  <!-- Step 1: Personal Information -->
  <div class="form-step active" id="step-1">
    <h2>Personal Information</h2>
    
    <div class="form-group">
      <label for="first-name">First Name</label>
      <input type="text" id="first-name" name="first-name" required>
      <span class="error" id="first-name-error"></span>
    </div>
    
    <div class="form-group">
      <label for="last-name">Last Name</label>
      <input type="text" id="last-name" name="last-name" required>
      <span class="error" id="last-name-error"></span>
    </div>
    
    <div class="form-group">
      <label for="birth-date">Date of Birth</label>
      <input type="date" id="birth-date" name="birth-date" required>
      <span class="error" id="birth-date-error"></span>
    </div>
    
    <div class="form-group">
      <label for="gender">Gender</label>
      <select id="gender" name="gender" required>
        <option value="">Select Gender</option>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="non-binary">Non-binary</option>
        <option value="other">Other</option>
        <option value="prefer-not">Prefer not to say</option>
      </select>
      <span class="error" id="gender-error"></span>
    </div>
    
    <div class="form-buttons">
      <button type="button" class="next-button" data-next="2">Next</button>
    </div>
  </div>
  
  <!-- Step 2: Contact Details -->
  <div class="form-step" id="step-2">
    <h2>Contact Details</h2>
    
    <div class="form-group">
      <label for="email">Email Address</label>
      <input type="email" id="email" name="email" required>
      <span class="error" id="email-error"></span>
    </div>
    
    <div class="form-group">
      <label for="phone">Phone Number</label>
      <input type="tel" id="phone" name="phone" required pattern="[0-9]{10}">
      <span class="hint">10-digit number without spaces or dashes</span>
      <span class="error" id="phone-error"></span>
    </div>
    
    <div class="form-group">
      <label for="address">Address</label>
      <textarea id="address" name="address" required></textarea>
      <span class="error" id="address-error"></span>
    </div>
    
    <div class="form-buttons">
      <button type="button" class="back-button" data-back="1">Back</button>
      <button type="button" class="next-button" data-next="3">Next</button>
    </div>
  </div>
  
  <!-- Step 3: Account Setup -->
  <div class="form-step" id="step-3">
    <h2>Account Setup</h2>
    
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text" id="username" name="username" required minlength="5" maxlength="20">
      <span class="hint">5-20 characters, letters and numbers only</span>
      <span class="error" id="username-error"></span>
    </div>
    
    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" id="password" name="password" required minlength="8">
      <span class="hint">At least 8 characters with uppercase, lowercase, number, and special character</span>
      <span class="error" id="password-error"></span>
    </div>
    
    <div class="form-group">
      <label for="confirm-password">Confirm Password</label>
      <input type="password" id="confirm-password" name="confirm-password" required>
      <span class="error" id="confirm-password-error"></span>
    </div>
    
    <div class="form-buttons">
      <button type="button" class="back-button" data-back="2">Back</button>
      <button type="button" class="next-button" data-next="4">Next</button>
    </div>
  </div>
  
  <!-- Step 4: Confirmation -->
  <div class="form-step" id="step-4">
    <h2>Confirm Your Information</h2>
    
    <div id="summary">
      <!-- Summary content will be populated by JavaScript -->
    </div>
    
    <div class="form-group checkbox-group">
      <input type="checkbox" id="terms" name="terms" required>
      <label for="terms">I agree to the <a href="#">Terms and Conditions</a></label>
      <span class="error" id="terms-error"></span>
    </div>
    
    <div class="form-buttons">
      <button type="button" class="back-button" data-back="3">Back</button>
      <button type="submit" class="submit-button">Submit</button>
    </div>
  </div>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('multi-step-form');
  const steps = document.querySelectorAll('.form-step');
  const progressSteps = document.querySelectorAll('.progress-step');
  
  // Initialize form validation data structure
  const formValidation = {
    step1: false,
    step2: false,
    step3: false,
    step4: false
  };
  
  // Next button event listeners
  const nextButtons = document.querySelectorAll('.next-button');
  nextButtons.forEach(button => {
    button.addEventListener('click', function() {
      const currentStep = parseInt(button.getAttribute('data-next')) - 1;
      const nextStep = parseInt(button.getAttribute('data-next'));
      
      // Validate current step
      if (validateStep(currentStep)) {
        formValidation['step' + currentStep] = true;
        
        // Move to next step
        showStep(nextStep);
        
        // If moving to confirmation step, populate summary
        if (nextStep === 4) {
          populateSummary();
        }
      }
    });
  });
  
  // Back button event listeners
  const backButtons = document.querySelectorAll('.back-button');
  backButtons.forEach(button => {
    button.addEventListener('click', function() {
      const prevStep = parseInt(button.getAttribute('data-back'));
      showStep(prevStep);
    });
  });
  
  // Form submission
  form.addEventListener('submit', function(event) {
    event.preventDefault();
    
    // Validate step 4
    if (validateStep(4)) {
      formValidation.step4 = true;
      
      // If all steps are valid, submit the form
      if (formValidation.step1 && formValidation.step2 && formValidation.step3 && formValidation.step4) {
        alert('Form submitted successfully!');
        console.log('Form data:', new FormData(form));
        
        // In a real application, you would submit the form data to your server here
        // form.submit();
      }
    }
  });
  
  // Function to show a specific step
  function showStep(stepNumber) {
    // Hide all steps
    steps.forEach(step => {
      step.classList.remove('active');
    });
    
    // Update progress indicators
    progressSteps.forEach(step => {
      const stepNum = parseInt(step.getAttribute('data-step'));
      
      if (stepNum < stepNumber) {
        step.classList.add('completed');
        step.classList.remove('active');
      } else if (stepNum === stepNumber) {
        step.classList.add('active');
        step.classList.remove('completed');
      } else {
        step.classList.remove('active', 'completed');
      }
    });
    
    // Show the current step
    document.getElementById('step-' + stepNumber).classList.add('active');
  }
  
  // Validate each step
  function validateStep(stepNumber) {
    let isValid = true;
    
    switch(stepNumber) {
      case 1:
        // Validate personal information
        isValid = validateField('first-name', 'First name is required') &&
                 validateField('last-name', 'Last name is required') &&
                 validateField('birth-date', 'Date of birth is required') &&
                 validateField('gender', 'Please select your gender');
        break;
        
      case 2:
        // Validate contact details
        isValid = validateField('email', 'Email address is required') &&
                 validateEmail() &&
                 validateField('phone', 'Phone number is required') &&
                 validatePhone() &&
                 validateField('address', 'Address is required');
        break;
        
      case 3:
        // Validate account setup
        isValid = validateField('username', 'Username is required') &&
                 validateUsername() &&
                 validateField('password', 'Password is required') &&
                 validatePassword() &&
                 validatePasswordMatch();
        break;
        
      case 4:
        // Validate terms agreement
        isValid = validateTerms();
        break;
    }
    
    return isValid;
  }
  
  // Validate a required field
  function validateField(fieldId, errorMessage) {
    const field = document.getElementById(fieldId);
    const errorElement = document.getElementById(fieldId + '-error');
    
    if (!field.value.trim()) {
      errorElement.textContent = errorMessage;
      field.classList.add('invalid');
      return false;
    } else {
      errorElement.textContent = '';
      field.classList.remove('invalid');
      field.classList.add('valid');
      return true;
    }
  }
  
  // Validate email format
  function validateEmail() {
    const email = document.getElementById('email');
    const errorElement = document.getElementById('email-error');
    
    if (email.value && !isValidEmail(email.value)) {
      errorElement.textContent = 'Please enter a valid email address';
      email.classList.add('invalid');
      return false;
    }
    return true;
  }
  
  // Validate phone format
  function validatePhone() {
    const phone = document.getElementById('phone');
    const errorElement = document.getElementById('phone-error');
    
    if (phone.value && !phone.validity.valid) {
      errorElement.textContent = 'Please enter a valid 10-digit phone number';
      phone.classList.add('invalid');
      return false;
    }
    return true;
  }
  
  // Validate username
  function validateUsername() {
    const username = document.getElementById('username');
    const errorElement = document.getElementById('username-error');
    
    if (username.value && username.value.length < 5) {
      errorElement.textContent = 'Username must be at least 5 characters';
      username.classList.add('invalid');
      return false;
    } else if (username.value && !/^[a-zA-Z0-9]+$/.test(username.value)) {
      errorElement.textContent = 'Username can only contain letters and numbers';
      username.classList.add('invalid');
      return false;
    }
    return true;
  }
  
  // Validate password
  function validatePassword() {
    const password = document.getElementById('password');
    const errorElement = document.getElementById('password-error');
    
    if (password.value && password.value.length < 8) {
      errorElement.textContent = 'Password must be at least 8 characters';
      password.classList.add('invalid');
      return false;
    } else if (password.value && !isStrongPassword(password.value)) {
      errorElement.textContent = 'Password must include uppercase, lowercase, number, and special character';
      password.classList.add('invalid');
      return false;
    }
    return true;
  }
  
  // Validate password match
  function validatePasswordMatch() {
    const password = document.getElementById('password');
    const confirmPassword = document.getElementById('confirm-password');
    const errorElement = document.getElementById('confirm-password-error');
    
    if (!confirmPassword.value) {
      errorElement.textContent = 'Please confirm your password';
      confirmPassword.classList.add('invalid');
      return false;
    } else if (confirmPassword.value !== password.value) {
      errorElement.textContent = 'Passwords do not match';
      confirmPassword.classList.add('invalid');
      return false;
    } else {
      errorElement.textContent = '';
      confirmPassword.classList.remove('invalid');
      confirmPassword.classList.add('valid');
      return true;
    }
  }
  
  // Validate terms agreement
  function validateTerms() {
    const terms = document.getElementById('terms');
    const errorElement = document.getElementById('terms-error');
    
    if (!terms.checked) {
      errorElement.textContent = 'You must agree to the terms and conditions';
      return false;
    } else {
      errorElement.textContent = '';
      return true;
    }
  }
  
  // Populate the summary section
  function populateSummary() {
    const summary = document.getElementById('summary');
    const firstName = document.getElementById('first-name').value;
    const lastName = document.getElementById('last-name').value;
    const birthDate = document.getElementById('birth-date').value;
    const gender = document.getElementById('gender').value;
    const email = document.getElementById('email').value;
    const phone = document.getElementById('phone').value;
    const address = document.getElementById('address').value;
    const username = document.getElementById('username').value;
    
    summary.innerHTML = `
      <div class="summary-section">
        <h3>Personal Information</h3>
        <p><strong>Name:</strong> ${firstName} ${lastName}</p>
        <p><strong>Date of Birth:</strong> ${formatDate(birthDate)}</p>
        <p><strong>Gender:</strong> ${formatGender(gender)}</p>
      </div>
      
      <div class="summary-section">
        <h3>Contact Details</h3>
        <p><strong>Email:</strong> ${email}</p>
        <p><strong>Phone:</strong> ${formatPhone(phone)}</p>
        <p><strong>Address:</strong> ${address}</p>
      </div>
      
      <div class="summary-section">
        <h3>Account Information</h3>
        <p><strong>Username:</strong> ${username}</p>
        <p><strong>Password:</strong> ********</p>
      </div>
    `;
  }
  
  // Helper function: Format date
  function formatDate(dateString) {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
  }
  
  // Helper function: Format gender
  function formatGender(gender) {
    if (gender === 'prefer-not') {
      return 'Prefer not to say';
    }
    return gender.charAt(0).toUpperCase() + gender.slice(1);
  }
  
  // Helper function: Format phone
  function formatPhone(phone) {
    return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
  }
  
  // Helper function: Validate email format
  function isValidEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  // Helper function: Check password strength
  function isStrongPassword(password) {
    // Password must contain at least one uppercase letter, one lowercase letter,
    // one number, and one special character
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])[A-Za-z\d!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{8,}$/;
    return passwordRegex.test(password);
  }
});
</script>

CSS for Validation Styling

/* Basic form styling */
.form-group {
  margin-bottom: 1.5rem;
  position: relative;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #333;
}

.form-group input, 
.form-group select, 
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
  transition: border-color 0.3s, box-shadow 0.3s;
}

.form-group input:focus, 
.form-group select:focus, 
.form-group textarea:focus {
  border-color: #4a90e2;
  box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
  outline: none;
}

/* Validation states */
.form-group input.valid, 
.form-group select.valid, 
.form-group textarea.valid {
  border-color: #28a745;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right calc(0.375em + 0.1875rem) center;
  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

.form-group input.invalid, 
.form-group select.invalid, 
.form-group textarea.invalid {
  border-color: #dc3545;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right calc(0.375em + 0.1875rem) center;
  background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

/* Error and hint messages */
.error {
  display: block;
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
  min-height: 1.25rem; /* Prevents layout shift */
}

.hint {
  display: block;
  color: #6c757d;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/* Checkbox styling */
.checkbox-group {
  display: flex;
  align-items: flex-start;
}

.checkbox-group input {
  width: auto;
  margin-right: 0.5rem;
  margin-top: 0.25rem;
}

.checkbox-group label {
  margin-bottom: 0;
}

/* Password strength meter */
.password-strength-meter {
  height: 5px;
  background-color: #f3f3f3;
  margin-top: 0.5rem;
  border-radius: 3px;
  overflow: hidden;
}

.password-strength-meter div {
  height: 100%;
  width: 0;
  transition: width 0.3s, background-color 0.3s;
}

.strength-weak {
  width: 25% !important;
  background-color: #dc3545;
}

.strength-medium {
  width: 50% !important;
  background-color: #ffc107;
}

.strength-strong {
  width: 75% !important;
  background-color: #17a2b8;
}

.strength-very-strong {
  width: 100% !important;
  background-color: #28a745;
}

/* Multi-step form specific styles */
.form-step {
  display: none;
  animation: fadeIn 0.5s;
}

.form-step.active {
  display: block;
}

.form-progress {
  display: flex;
  margin-bottom: 2rem;
  position: relative;
}

.form-progress::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  height: 2px;
  width: 100%;
  background-color: #e9ecef;
  z-index: 1;
}

.progress-step {
  position: relative;
  z-index: 2;
  flex: 1;
  text-align: center;
  padding-top: 30px;
  font-size: 0.875rem;
  color: #6c757d;
}

.progress-step::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 20px;
  height: 20px;
  background-color: #fff;
  border: 2px solid #e9ecef;
  border-radius: 50%;
}

.progress-step.active::before {
  border-color: #4a90e2;
  background-color: #4a90e2;
}

.progress-step.completed::before {
  border-color: #28a745;
  background-color: #28a745;
}

.progress-step.active {
  color: #4a90e2;
}

.progress-step.completed {
  color: #28a745;
}

.form-buttons {
  display: flex;
  justify-content: space-between;
  margin-top: 2rem;
}

.form-buttons button {
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  border: none;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.3s;
}

.back-button {
  background-color: #f8f9fa;
  color: #212529;
}

.back-button:hover {
  background-color: #e9ecef;
}

.next-button, .submit-button {
  background-color: #4a90e2;
  color: white;
}

.next-button:hover, .submit-button:hover {
  background-color: #357ab8;
}

/* Summary section styling */
.summary-section {
  background-color: #f8f9fa;
  border-radius: 4px;
  padding: 1rem;
  margin-bottom: 1rem;
}

.summary-section h3 {
  margin-top: 0;
  margin-bottom: 0.5rem;
  font-size: 1.1rem;
  color: #212529;
}

/* Animation */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

Additional Resources

Key Takeaways

  • Form validation ensures data quality, improves user experience, and enhances security
  • Always combine client-side validation with server-side validation
  • HTML5 offers built-in validation attributes for simple validation needs
  • JavaScript validation provides complete control over validation logic and user experience
  • The Constraint Validation API bridges HTML5 and JavaScript for more customized validation
  • Always provide clear, helpful error messages that guide users to fix issues
  • Consider accessibility by using appropriate ARIA attributes and ensuring validation feedback is accessible to all users
  • Real-time validation provides immediate feedback and improves user experience
  • For complex forms, consider a step-by-step validation approach
  • Test your form validation across different browsers, devices, and assistive technologies