← Back to DevBytes

Implementing HTML Forms and Validation in Modern Web Applications

Understanding HTML Forms and Validation

HTML forms are the primary mechanism for collecting user input on the web. From login screens and search bars to complex multi-step wizards and checkout flows, forms power the interactive layer of nearly every web application. Validation ensures that the data submitted through these forms is complete, accurate, and safe before it reaches your server or database.

Modern web development offers a layered approach to form validation: native HTML5 attributes for declarative rules, the Constraint Validation API for programmatic control, and custom JavaScript for business logic that goes beyond built-in capabilities. Understanding how these layers work together is essential for building robust, user-friendly forms.

What Makes a Modern Form

A modern HTML form is more than just a collection of <input> elements wrapped in a <form> tag. It incorporates semantic structure, accessibility features, real-time feedback, and progressive enhancement. The foundation remains remarkably simple:

<form action="/submit" method="post" novalidate>
  <fieldset>
    <legend>Contact Information</legend>
    
    <div class="form-group">
      <label for="name">Full Name</label>
      <input 
        type="text" 
        id="name" 
        name="name" 
        required 
        minlength="2"
        autocomplete="name"
      >
    </div>
    
    <div class="form-group">
      <label for="email">Email Address</label>
      <input 
        type="email" 
        id="email" 
        name="email" 
        required 
        autocomplete="email"
      >
    </div>
    
    <div class="form-group">
      <label for="phone">Phone Number</label>
      <input 
        type="tel" 
        id="phone" 
        name="phone" 
        pattern="[0-9]{10,}" 
        autocomplete="tel"
      >
      <span class="help-text">Enter 10 or more digits, no spaces</span>
    </div>
  </fieldset>
  
  <button type="submit">Send Message</button>
</form>

Why Validation Matters

Validation serves three critical purposes that extend far beyond simply checking if a field is filled in:

HTML5 Built-in Validation Attributes

The most underutilized validation tools are already baked into HTML5. These declarative attributes require zero JavaScript and work across all modern browsers:

<!-- Required field -->
<input type="text" required>

<!-- Minimum and maximum length -->
<input type="text" minlength="3" maxlength="100">

<!-- Numeric range constraints -->
<input type="number" min="18" max="120" step="1">

<!-- Pattern matching with regex -->
<input type="text" pattern="[A-Z]{2}[0-9]{4}" 
       title="Format: Two uppercase letters followed by four digits">

<!-- Specialized input types with built-in validation -->
<input type="email">      <!-- Validates email format -->
<input type="url">        <!-- Requires http(s):// prefix -->
<input type="date">       <!-- Browser date picker with format enforcement -->
<input type="time">       <!-- Time selection -->
<input type="number">     <!-- Numeric keyboard on mobile + value validation -->
<input type="tel">        <!-- Telephone keyboard on mobile -->
<input type="search">     <!-- Search-optimized input -->

The pattern attribute is particularly powerful. It accepts any regular expression and automatically validates the input against it. Always pair pattern with a title attribute—the title text appears in the browser's default validation tooltip when the pattern fails, giving users a human-readable explanation of what went wrong.

The Constraint Validation API

When declarative attributes aren't enough—for example, when validation depends on the relationship between multiple fields—the Constraint Validation API provides programmatic access to the form's validation state. Every form control exposes a set of properties and methods that let you inspect validity, set custom error messages, and control when validation feedback appears.

Here is a complete working example that demonstrates the API in action:

<form id="signup-form" novalidate>
  <div class="form-group">
    <label for="password">Password</label>
    <input 
      type="password" 
      id="password" 
      name="password" 
      required 
      minlength="8"
    >
    <div class="error-message" id="password-error"></div>
  </div>
  
  <div class="form-group">
    <label for="confirm-password">Confirm Password</label>
    <input 
      type="password" 
      id="confirm-password" 
      name="confirm_password" 
      required
    >
    <div class="error-message" id="confirm-error"></div>
  </div>
  
  <button type="submit">Create Account</button>
</form>

<script>
  const form = document.getElementById('signup-form');
  const password = document.getElementById('password');
  const confirmPassword = document.getElementById('confirm-password');
  const passwordError = document.getElementById('password-error');
  const confirmError = document.getElementById('confirm-error');

  // Clear default HTML5 validation to use our custom approach
  form.setAttribute('novalidate', 'novalidate');

  function validatePasswordStrength(value) {
    const hasUpperCase = /[A-Z]/.test(value);
    const hasLowerCase = /[a-z]/.test(value);
    const hasNumber = /[0-9]/.test(value);
    const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
    const isLongEnough = value.length >= 8;
    
    const missing = [];
    if (!hasUpperCase) missing.push('an uppercase letter');
    if (!hasLowerCase) missing.push('a lowercase letter');
    if (!hasNumber) missing.push('a number');
    if (!hasSpecial) missing.push('a special character');
    if (!isLongEnough) missing.push('at least 8 characters');
    
    return missing;
  }

  function showFieldError(inputElement, errorElement, message) {
    inputElement.classList.add('invalid');
    inputElement.classList.remove('valid');
    errorElement.textContent = message;
    errorElement.style.display = 'block';
  }

  function showFieldSuccess(inputElement, errorElement) {
    inputElement.classList.remove('invalid');
    inputElement.classList.add('valid');
    errorElement.textContent = '';
    errorElement.style.display = 'none';
  }

  // Real-time validation on blur (when user leaves the field)
  password.addEventListener('blur', () => {
    const value = password.value;
    
    if (!value) {
      showFieldError(password, passwordError, 'Password is required');
      return;
    }
    
    const missingRequirements = validatePasswordStrength(value);
    
    if (missingRequirements.length > 0) {
      const message = 'Password must include: ' + missingRequirements.join(', ') + '.';
      showFieldError(password, passwordError, message);
    } else {
      showFieldSuccess(password, passwordError);
    }
    
    // Re-validate confirm password if it has a value
    if (confirmPassword.value) {
      validateConfirmField();
    }
  });

  function validateConfirmField() {
    if (!confirmPassword.value) {
      showFieldError(confirmPassword, confirmError, 'Please confirm your password');
      return false;
    }
    
    if (confirmPassword.value !== password.value) {
      showFieldError(confirmPassword, confirmError, 'Passwords do not match');
      return false;
    }
    
    showFieldSuccess(confirmPassword, confirmError);
    return true;
  }

  confirmPassword.addEventListener('blur', validateConfirmField);
  
  // Live validation as user types (with debounce for performance)
  let debounceTimer;
  password.addEventListener('input', () => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      if (password.value) {
        const missingRequirements = validatePasswordStrength(password.value);
        if (missingRequirements.length === 0) {
          showFieldSuccess(password, passwordError);
        } else {
          const message = 'Still need: ' + missingRequirements.join(', ') + '.';
          showFieldError(password, passwordError, message);
        }
        if (confirmPassword.value) validateConfirmField();
      }
    }, 300);
  });

  // Final validation on submit
  form.addEventListener('submit', (event) => {
    let isValid = true;
    
    // Validate password field using Constraint Validation API
    if (!password.validity.valid) {
      event.preventDefault();
      isValid = false;
      
      if (password.validity.valueMissing) {
        showFieldError(password, passwordError, 'Password is required');
      } else if (password.validity.tooShort) {
        showFieldError(password, passwordError, 
          'Password must be at least 8 characters (currently ' + password.value.length + ')');
      }
    }
    
    // Custom cross-field validation
    if (!validateConfirmField()) {
      event.preventDefault();
      isValid = false;
    }
    
    if (isValid) {
      console.log('Form submitted successfully');
      // Form will submit normally
    } else {
      // Focus the first invalid field for accessibility
      const firstInvalid = form.querySelector('.invalid');
      if (firstInvalid) firstInvalid.focus();
    }
  });

  // Accessibility: announce errors to screen readers
  function announceError(message) {
    const liveRegion = document.getElementById('form-announcements');
    if (liveRegion) {
      liveRegion.textContent = message;
    }
  }
</script>

Custom Validation with the setCustomValidity Method

The setCustomValidity() method is the bridge between native validation and your custom logic. When you call this method with a non-empty string, the field becomes invalid and the browser displays your custom message. Call it with an empty string to clear the custom error and let the browser fall back to its built-in validation messages:

<input type="text" id="username" name="username" required>
<div class="error-container" id="username-error"></div>

<script>
  const usernameInput = document.getElementById('username');
  const usernameError = document.getElementById('username-error');
  
  // Simulate an asynchronous check (e.g., checking username availability)
  async function checkUsernameAvailability(username) {
    // Simulated API call - in reality this would hit your backend
    const takenUsernames = ['admin', 'root', 'system', 'moderator'];
    
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(!takenUsernames.includes(username.toLowerCase()));
      }, 500);
    });
  }
  
  usernameInput.addEventListener('blur', async () => {
    const value = usernameInput.value.trim();
    
    // Clear any previous custom validation
    usernameInput.setCustomValidity('');
    usernameError.textContent = '';
    
    if (!value) return; // required attribute handles empty case
    
    // Check length constraints
    if (value.length < 3) {
      usernameInput.setCustomValidity('Username must be at least 3 characters');
      usernameError.textContent = 'Username must be at least 3 characters';
      return;
    }
    
    if (value.length > 30) {
      usernameInput.setCustomValidity('Username must be 30 characters or fewer');
      usernameError.textContent = 'Username must be 30 characters or fewer';
      return;
    }
    
    // Check for valid characters
    const validPattern = /^[a-zA-Z0-9_]+$/;
    if (!validPattern.test(value)) {
      usernameInput.setCustomValidity('Only letters, numbers, and underscores allowed');
      usernameError.textContent = 'Only letters, numbers, and underscores allowed';
      return;
    }
    
    // Asynchronous availability check
    const isAvailable = await checkUsernameAvailability(value);
    
    if (!isAvailable) {
      usernameInput.setCustomValidity('This username is already taken');
      usernameError.textContent = 'This username is already taken. Please choose another.';
    } else {
      usernameInput.setCustomValidity(''); // Valid!
      usernameError.textContent = '';
      usernameInput.classList.add('username-available');
    }
  });
</script>

Styling Validation States with CSS

CSS provides pseudo-classes that target form validation states, enabling visual feedback without additional JavaScript classes. Combine these with custom styling to create an intuitive validation experience:

<style>
  /* Base input styling */
  .form-group input,
  .form-group select,
  .form-group textarea {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 2px solid #d1d5db;
    border-radius: 0.5rem;
    font-size: 1rem;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
    outline: none;
    background: #fff;
  }
  
  /* Focus state - neutral, before validation */
  .form-group input:focus,
  .form-group select:focus,
  .form-group textarea:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
  }
  
  /* Valid state - green border and subtle green glow */
  .form-group input:valid:not(:placeholder-shown),
  .form-group textarea:valid:not(:placeholder-shown) {
    border-color: #10b981;
    background: linear-gradient(to right, #f0fdf4 0%, #fff 30%);
  }
  
  .form-group input:valid:not(:placeholder-shown):focus,
  .form-group textarea:valid:not(:placeholder-shown):focus {
    border-color: #059669;
    box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
  }
  
  /* Invalid state - red border and red glow */
  .form-group input:invalid:not(:placeholder-shown),
  .form-group textarea:invalid:not(:placeholder-shown) {
    border-color: #ef4444;
    background: linear-gradient(to right, #fef2f2 0%, #fff 30%);
  }
  
  .form-group input:invalid:not(:placeholder-shown):focus,
  .form-group textarea:invalid:not(:placeholder-shown):focus {
    border-color: #dc2626;
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
  }
  
  /* Custom classes for programmatic validation */
  .form-group input.invalid {
    border-color: #ef4444 !important;
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15) !important;
  }
  
  .form-group input.valid {
    border-color: #10b981 !important;
    box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15) !important;
  }
  
  /* Error and success messages */
  .error-message {
    color: #dc2626;
    font-size: 0.875rem;
    margin-top: 0.375rem;
    display: none;
    align-items: center;
    gap: 0.375rem;
  }
  
  .error-message::before {
    content: '⚠';
    font-size: 1rem;
  }
  
  .success-message {
    color: #059669;
    font-size: 0.875rem;
    margin-top: 0.375rem;
    display: none;
    align-items: center;
    gap: 0.375rem;
  }
  
  .success-message::before {
    content: '✓';
    font-size: 1rem;
  }
  
  /* Submit button states */
  button[type="submit"]:disabled {
    opacity: 0.6;
    cursor: not-allowed;
    background: #9ca3af;
  }
  
  button[type="submit"]:not(:disabled) {
    background: #3b82f6;
    color: white;
    cursor: pointer;
    transition: background 0.2s;
  }
  
  button[type="submit"]:not(:disabled):hover {
    background: #2563eb;
  }
  
  /* Help text styling */
  .help-text {
    color: #6b7280;
    font-size: 0.8rem;
    margin-top: 0.25rem;
  }
</style>

The :not(:placeholder-shown) selector is crucial—it prevents validation styles from appearing on empty fields that the user hasn't interacted with yet. Without this guard, required fields would show red borders on page load, creating a hostile first impression.

Complete Multi-Field Form Example

Let's combine everything into a realistic registration form with comprehensive validation, including a checkbox for terms acceptance and a select dropdown:

<form id="registration-form" novalidate>
  
  <!-- Screen reader live region for accessibility announcements -->
  <div id="form-announcements" class="sr-only" aria-live="assertive" aria-atomic="true"></div>
  
  <fieldset>
    <legend>Account Details</legend>
    
    <div class="form-group">
      <label for="reg-email">Email Address *</label>
      <input 
        type="email" 
        id="reg-email" 
        name="email" 
        required 
        autocomplete="email"
        aria-describedby="email-help email-error"
      >
      <span class="help-text" id="email-help">We'll never share your email</span>
      <div class="error-message" id="email-error" role="alert"></div>
    </div>
    
    <div class="form-group">
      <label for="reg-username">Username *</label>
      <input 
        type="text" 
        id="reg-username" 
        name="username" 
        required 
        minlength="3" 
        maxlength="30"
        pattern="[a-zA-Z0-9_]+"
        title="Letters, numbers, and underscores only"
        autocomplete="username"
        aria-describedby="username-help username-error"
      >
      <span class="help-text" id="username-help">3-30 characters, letters/numbers/underscores</span>
      <div class="error-message" id="username-error" role="alert"></div>
    </div>
    
    <div class="form-group">
      <label for="reg-password">Password *</label>
      <input 
        type="password" 
        id="reg-password" 
        name="password" 
        required 
        minlength="8"
        autocomplete="new-password"
        aria-describedby="password-help password-error"
      >
      <span class="help-text" id="password-help">Minimum 8 characters with uppercase, lowercase, number, and special character</span>
      <div class="error-message" id="password-error" role="alert"></div>
      <div class="password-strength-meter">
        <div class="strength-bar" id="strength-bar"></div>
      </div>
      <span class="strength-label" id="strength-label"></span>
    </div>
    
    <div class="form-group">
      <label for="reg-confirm">Confirm Password *</label>
      <input 
        type="password" 
        id="reg-confirm" 
        name="confirm_password" 
        required
        autocomplete="new-password"
        aria-describedby="confirm-error"
      >
      <div class="error-message" id="confirm-error" role="alert"></div>
    </div>
  </fieldset>
  
  <fieldset>
    <legend>Personal Information</legend>
    
    <div class="form-group">
      <label for="reg-name">Full Name *</label>
      <input 
        type="text" 
        id="reg-name" 
        name="full_name" 
        required 
        minlength="2"
        autocomplete="name"
        aria-describedby="name-error"
      >
      <div class="error-message" id="name-error" role="alert"></div>
    </div>
    
    <div class="form-group">
      <label for="reg-country">Country *</label>
      <select 
        id="reg-country" 
        name="country" 
        required
        aria-describedby="country-error"
      >
        <option value="">Select your country</option>
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="uk">United Kingdom</option>
        <option value="au">Australia</option>
        <option value="de">Germany</option>
        <option value="fr">France</option>
        <option value="jp">Japan</option>
        <option value="other">Other</option>
      </select>
      <div class="error-message" id="country-error" role="alert"></div>
    </div>
  </fieldset>
  
  <fieldset>
    <legend>Agreement</legend>
    
    <div class="form-group checkbox-group">
      <input 
        type="checkbox" 
        id="reg-terms" 
        name="terms_accepted" 
        required
        aria-describedby="terms-error"
      >
      <label for="reg-terms" class="checkbox-label">
        I agree to the Terms of Service and Privacy Policy *
      </label>
      <div class="error-message" id="terms-error" role="alert"></div>
    </div>
    
    <div class="form-group checkbox-group">
      <input 
        type="checkbox" 
        id="reg-newsletter" 
        name="newsletter_subscribe"
      >
      <label for="reg-newsletter" class="checkbox-label">
        Send me occasional product updates and tips (optional)
      </label>
    </div>
  </fieldset>
  
  <div class="form-actions">
    <button type="submit" id="submit-btn">Create Account</button>
    <button type="reset" class="secondary-btn">Clear Form</button>
  </div>
  
  <div id="form-summary" class="form-summary" aria-live="polite"></div>
</form>

<script>
  (function() {
    const form = document.getElementById('registration-form');
    const fields = {
      email: {
        element: document.getElementById('reg-email'),
        error: document.getElementById('email-error'),
        validate(value) {
          if (!value) return 'Email address is required';
          if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
            return 'Please enter a valid email address (e.g., user@example.com)';
          }
          return null; // null means valid
        }
      },
      username: {
        element: document.getElementById('reg-username'),
        error: document.getElementById('username-error'),
        validate(value) {
          if (!value) return 'Username is required';
          if (value.length < 3) return 'Username must be at least 3 characters';
          if (value.length > 30) return 'Username must be 30 characters or fewer';
          if (!/^[a-zA-Z0-9_]+$/.test(value)) {
            return 'Only letters, numbers, and underscores allowed';
          }
          return null;
        }
      },
      password: {
        element: document.getElementById('reg-password'),
        error: document.getElementById('password-error'),
        validate(value) {
          if (!value) return 'Password is required';
          if (value.length < 8) return 'Password must be at least 8 characters';
          const checks = [
            [/[A-Z]/, 'uppercase letter'],
            [/[a-z]/, 'lowercase letter'],
            [/[0-9]/, 'number'],
            [/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, 'special character']
          ];
          const missing = checks.filter(([regex]) => !regex.test(value))
            .map(([, name]) => name);
          if (missing.length) {
            return `Password needs: ${missing.join(', ')}`;
          }
          return null;
        }
      },
      confirm: {
        element: document.getElementById('reg-confirm'),
        error: document.getElementById('confirm-error'),
        validate(value) {
          if (!value) return 'Please confirm your password';
          const passwordValue = fields.password.element.value;
          if (value !== passwordValue) return 'Passwords do not match';
          return null;
        }
      },
      name: {
        element: document.getElementById('reg-name'),
        error: document.getElementById('name-error'),
        validate(value) {
          if (!value || value.trim().length === 0) return 'Full name is required';
          if (value.trim().length < 2) return 'Name must be at least 2 characters';
          return null;
        }
      },
      country: {
        element: document.getElementById('reg-country'),
        error: document.getElementById('country-error'),
        validate(value) {
          if (!value || value === '') return 'Please select your country';
          return null;
        }
      },
      terms: {
        element: document.getElementById('reg-terms'),
        error: document.getElementById('terms-error'),
        validate(checked) {
          if (!checked) return 'You must accept the Terms of Service to continue';
          return null;
        }
      }
    };

    // Password strength meter
    const strengthBar = document.getElementById('strength-bar');
    const strengthLabel = document.getElementById('strength-label');
    
    function calculateStrength(password) {
      let score = 0;
      if (password.length >= 8) score++;
      if (password.length >= 12) score++;
      if (/[A-Z]/.test(password)) score++;
      if (/[a-z]/.test(password)) score++;
      if (/[0-9]/.test(password)) score++;
      if (/[^a-zA-Z0-9]/.test(password)) score++;
      if (password.length >= 16) score++;
      return Math.min(score, 5);
    }
    
    function updateStrengthMeter(password) {
      if (!password) {
        strengthBar.style.width = '0%';
        strengthLabel.textContent = '';
        return;
      }
      const strength = calculateStrength(password);
      const percentage = (strength / 5) * 100;
      strengthBar.style.width = percentage + '%';
      
      const labels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong', 'Very Strong'];
      const colors = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#16a34a', '#15803d'];
      strengthBar.style.backgroundColor = colors[strength];
      strengthLabel.textContent = labels[strength];
      strengthLabel.style.color = colors[strength];
    }

    fields.password.element.addEventListener('input', () => {
      updateStrengthMeter(fields.password.element.value);
    });

    function showError(fieldName, message) {
      const field = fields[fieldName];
      field.element.classList.add('invalid');
      field.element.classList.remove('valid');
      field.error.textContent = message;
      field.error.style.display = 'flex';
      // Set custom validity for Constraint Validation API
      field.element.setCustomValidity(message);
    }
    
    function clearError(fieldName) {
      const field = fields[fieldName];
      field.element.classList.remove('invalid');
      field.element.classList.add('valid');
      field.error.textContent = '';
      field.error.style.display = 'none';
      field.element.setCustomValidity('');
    }
    
    // Validate individual field
    function validateField(fieldName) {
      const field = fields[fieldName];
      let value;
      if (field.element.type === 'checkbox') {
        value = field.element.checked;
      } else {
        value = field.element.value;
      }
      const errorMessage = field.validate(value);
      if (errorMessage) {
        showError(fieldName, errorMessage);
        return false;
      } else {
        clearError(fieldName);
        return true;
      }
    }
    
    // Attach blur validators
    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      field.element.addEventListener('blur', () => {
        validateField(fieldName);
        // Cross-field dependencies
        if (fieldName === 'password' && fields.confirm.element.value) {
          validateField('confirm');
        }
      });
      
      // Live validation for specific fields
      if (['email', 'username', 'password'].includes(fieldName)) {
        let debounce;
        field.element.addEventListener('input', () => {
          clearTimeout(debounce);
          debounce = setTimeout(() => {
            validateField(fieldName);
            if (fieldName === 'password' && fields.confirm.element.value) {
              validateField('confirm');
            }
          }, 400);
        });
      }
      
      // Validate confirm field when it changes
      if (fieldName === 'confirm') {
        field.element.addEventListener('input', () => {
          clearTimeout(debounce);
          debounce = setTimeout(() => validateField('confirm'), 300);
        });
      }
      
      // Checkbox and select need immediate validation
      if (['terms', 'country'].includes(fieldName)) {
        field.element.addEventListener('change', () => {
          validateField(fieldName);
        });
      }
    });
    
    // Form submission handler
    form.addEventListener('submit', (event) => {
      let isFormValid = true;
      const summary = document.getElementById('form-summary');
      
      Object.keys(fields).forEach(fieldName => {
        const valid = validateField(fieldName);
        if (!valid) isFormValid = false;
      });
      
      if (!isFormValid) {
        event.preventDefault();
        
        // Count errors for summary
        const errorCount = Object.values(fields)
          .filter(f => f.element.classList.contains('invalid'))
          .length;
        
        summary.textContent = `Please fix ${errorCount} ${errorCount === 1 ? 'error' : 'errors'} above before submitting.`;
        summary.style.color = '#dc2626';
        
        // Focus first invalid field
        const firstInvalid = form.querySelector('.invalid');
        if (firstInvalid) firstInvalid.focus();
        
        // Announce to screen readers
        document.getElementById('form-announcements').textContent = 
          `Form has ${errorCount} ${errorCount === 1 ? 'error' : 'errors'}. Please correct them.`;
      } else {
        summary.textContent = 'All fields look good! Creating your account...';
        summary.style.color = '#059669';
      }
    });
    
    // Reset handler
    form.addEventListener('reset', () => {
      setTimeout(() => {
        Object.keys(fields).forEach(fieldName => clearError(fieldName));
        updateStrengthMeter('');
        document.getElementById('form-summary').textContent = '';
        document.getElementById('form-announcements').textContent = 'Form has been cleared';
      }, 0);
    });
    
    // Disable submit button if form has critical errors
    function updateSubmitButtonState() {
      const submitBtn = document.getElementById('submit-btn');
      const hasErrors = form.querySelectorAll('.invalid').length > 0;
      submitBtn.disabled = hasErrors;
    }
    
    // Observer for class changes to update button state
    const observer = new MutationObserver(updateSubmitButtonState);
    observer.observe(form, { subtree: true, attributes: true, attributeFilter: ['class'] });
  })();
</script>

Server-Side Validation: The Non-Negotiable Layer

Client-side validation is a convenience, never a security boundary. Users

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles