DEV Community

Cover image for 8 JavaScript Techniques for Building Accessible Form Validation
Aarav Joshi
Aarav Joshi

Posted on

8 JavaScript Techniques for Building Accessible Form Validation

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

JavaScript has revolutionized web form validation, enabling developers to create responsive, user-friendly experiences. However, building truly accessible validation systems requires careful consideration and implementation. In this article, I'll share eight powerful techniques to enhance form validation accessibility while maintaining a seamless user experience for everyone.

Form validation serves as the gateway between users and your application. When implemented thoughtfully, it guides users toward successful form completion rather than creating frustration. I've found that accessible validation isn't just about accommodating screen readers—it benefits all users by providing clear, consistent feedback.

Form Validation Architecture

A robust validation architecture requires planning beyond simple error messages. Creating a system that communicates through multiple channels ensures all users receive appropriate feedback.

My approach centers on a layered validation system. At its foundation are HTML5 native validation attributes like required, pattern, and minlength. I enhance these with JavaScript for custom validation logic and ARIA attributes to improve accessibility.

// Basic form validation architecture
function initFormValidation(formSelector) {
  const form = document.querySelector(formSelector);
  if (!form) return;

  // Store references to form elements
  const formElements = Array.from(form.elements);

  // Initialize validation state
  const validationState = {
    isValid: false,
    errors: new Map()
  };

  // Add event listeners
  form.addEventListener('submit', handleSubmit);
  formElements.forEach(element => {
    if (element.tagName !== 'BUTTON') {
      element.addEventListener('blur', handleBlur);
      element.addEventListener('input', handleInput);
    }
  });

  function handleSubmit(event) {
    validateAllFields();

    if (!validationState.isValid) {
      event.preventDefault();
      announceValidationErrors();
      focusFirstInvalidField();
    }
  }

  // Validation logic follows...
}
Enter fullscreen mode Exit fullscreen mode

This foundation establishes the key components of accessible validation: form-wide validation, per-field validation, and appropriate user feedback mechanisms.

Input Pattern Validation

Regular expressions provide powerful input validation capabilities, but they must be implemented carefully to maintain accessibility. I've learned that the most effective approach combines clear pattern requirements with helpful error messages.

When creating pattern validation, I define human-readable descriptions alongside technical validation:

const patternValidators = {
  email: {
    regex: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
    message: 'Please enter a valid email address (example@domain.com)'
  },
  phone: {
    regex: /^(\+\d{1,3})?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/,
    message: 'Please enter a valid phone number (e.g., 555-123-4567)'
  },
  zipCode: {
    regex: /^\d{5}(-\d{4})?$/,
    message: 'Please enter a valid ZIP code (e.g., 12345 or 12345-6789)'
  }
};

function validatePattern(field) {
  // Skip if empty and not required
  if (!field.value && !field.required) return true;

  const patternType = field.dataset.patternType;
  if (!patternType || !patternValidators[patternType]) return true;

  const validator = patternValidators[patternType];
  const isValid = validator.regex.test(field.value);

  if (!isValid) {
    showFieldError(field, validator.message);
  }

  return isValid;
}
Enter fullscreen mode Exit fullscreen mode

This approach provides users with clear expectations and reduces frustration. The predefined patterns also ensure consistency across your application.

Real-time Validation Feedback

Immediate feedback helps users correct errors as they complete forms, but timing is crucial. I've found that validating too early frustrates users, while delayed validation allows errors to accumulate.

My solution implements a balanced approach:

function handleInput(event) {
  const field = event.target;

  // Clear existing errors to avoid irritating users while typing
  clearFieldError(field);

  // If the field was previously invalid and now valid, announce the change
  if (field.getAttribute('aria-invalid') === 'true' && validateField(field)) {
    field.removeAttribute('aria-invalid');
    announceFieldValid(field);
  }

  // Only validate completed fields or fields the user has already attempted
  if (field.dataset.attempted === 'true' && field.value) {
    validateFieldWithDelay(field);
  }
}

function validateFieldWithDelay(field) {
  clearTimeout(field.validationTimeout);
  field.validationTimeout = setTimeout(() => {
    validateField(field);
  }, 500); // Delay validation to avoid interrupting typing
}
Enter fullscreen mode Exit fullscreen mode

This debounced validation provides timely feedback without disrupting the user experience. It respects the user's input flow while still providing guidance.

Error Message Management

Clear error messages are essential for all users, but particularly for those using assistive technology. I ensure error messages are programmatically associated with their respective fields and announced appropriately.

function showFieldError(field, message) {
  // Mark field as invalid
  field.setAttribute('aria-invalid', 'true');
  field.dataset.attempted = 'true';

  // Create or update error message
  let errorElement = document.getElementById(`error-${field.id}`);

  if (!errorElement) {
    errorElement = document.createElement('div');
    errorElement.id = `error-${field.id}`;
    errorElement.className = 'form-error';
    errorElement.setAttribute('aria-live', 'polite');
    field.parentNode.appendChild(errorElement);

    // Associate error with field
    field.setAttribute('aria-describedby', errorElement.id);
  }

  errorElement.textContent = message;

  // Add visual indication
  field.classList.add('invalid-input');

  // Add to validation state
  validationState.errors.set(field.id, message);
  validationState.isValid = false;
}

function clearFieldError(field) {
  const errorElement = document.getElementById(`error-${field.id}`);
  if (errorElement) {
    errorElement.textContent = '';
  }

  // Clean up aria and visual indicators when appropriate
  if (validationState.errors.has(field.id)) {
    validationState.errors.delete(field.id);

    // Only remove visual indicators if the field is now valid
    if (validateField(field, true)) {
      field.classList.remove('invalid-input');
      field.removeAttribute('aria-invalid');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Using aria-describedby connects error messages to their fields for screen readers, while aria-live ensures changes are announced. The visual indicators provide feedback for sighted users.

Form Submission Handling

Form submission is the culmination of the user's effort. Handling this step accessibly requires clear communication about validation status and submission progress.

async function handleSubmit(event) {
  event.preventDefault();

  // Validate all fields
  validateAllFields();

  if (!validationState.isValid) {
    announceValidationErrors();
    focusFirstInvalidField();
    return;
  }

  // Show loading state
  const submitButton = form.querySelector('[type="submit"]');
  const originalText = submitButton.textContent;
  submitButton.disabled = true;
  submitButton.textContent = 'Submitting...';

  // Add a loading announcement for screen readers
  const loadingAnnouncement = document.createElement('div');
  loadingAnnouncement.className = 'sr-only';
  loadingAnnouncement.setAttribute('aria-live', 'assertive');
  loadingAnnouncement.textContent = 'Form is submitting. Please wait.';
  form.appendChild(loadingAnnouncement);

  try {
    // Submit form data
    const formData = new FormData(form);
    const response = await submitFormData(formData);

    // Handle successful submission
    handleSuccessfulSubmission(response);
  } catch (error) {
    // Handle submission error
    handleSubmissionError(error);
  } finally {
    // Restore button state
    submitButton.disabled = false;
    submitButton.textContent = originalText;

    // Update loading announcement
    loadingAnnouncement.remove();
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach provides feedback for all users during submission, including those using screen readers. The loading state prevents multiple submissions while informing users about the form's status.

Custom Constraint Validation

The browser's Constraint Validation API provides powerful native validation capabilities that can be extended with custom logic. By leveraging this API rather than replacing it, I maintain browser compatibility while adding custom functionality.

function validateCustomConstraints(field) {
  // Use the Constraint Validation API for custom validation
  if (field.validity.valid) {
    // Field passes built-in validation, now check custom constraints

    const customValidator = field.dataset.validator;
    if (!customValidator) return true;

    let isValid = true;
    let message = '';

    switch (customValidator) {
      case 'password-strength':
        isValid = validatePasswordStrength(field.value);
        message = 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character';
        break;
      case 'future-date':
        isValid = validateFutureDate(field.value);
        message = 'Please select a date in the future';
        break;
      // Add other custom validators as needed
    }

    if (!isValid) {
      // Use the Constraint Validation API to set custom validity
      field.setCustomValidity(message);
      showFieldError(field, message);
    } else {
      field.setCustomValidity('');
    }

    return isValid;
  } else {
    // Get the built-in validation error message
    const message = field.validationMessage;
    showFieldError(field, message);
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

This integration with the Constraint Validation API ensures consistent behavior across browsers while enabling custom validation logic.

Grouped Input Validation

Many forms contain related fields that must be validated together. I've developed patterns to handle these relationships clearly and accessibly.

function validateFieldGroup(groupName) {
  const groupFields = Array.from(form.querySelectorAll(`[data-group="${groupName}"]`));
  if (!groupFields.length) return true;

  // Get group container for error placement
  const groupContainer = groupFields[0].closest('.field-group') || groupFields[0].parentNode;

  let isValid = true;
  let errorMessage = '';

  switch (groupName) {
    case 'password-confirmation': {
      const password = groupFields.find(f => f.name === 'password');
      const confirmation = groupFields.find(f => f.name === 'password_confirmation');

      if (password && confirmation && password.value !== confirmation.value) {
        isValid = false;
        errorMessage = 'Passwords do not match';
      }
      break;
    }
    case 'credit-card': {
      // Validate credit card details as a group
      const cardNumber = groupFields.find(f => f.name === 'card_number');
      const expiryMonth = groupFields.find(f => f.name === 'expiry_month');
      const expiryYear = groupFields.find(f => f.name === 'expiry_year');
      const cvv = groupFields.find(f => f.name === 'cvv');

      // Check expiration date
      if (expiryMonth && expiryYear) {
        const isExpiryValid = validateCreditCardExpiry(expiryMonth.value, expiryYear.value);
        if (!isExpiryValid) {
          isValid = false;
          errorMessage = 'Credit card is expired';
        }
      }
      break;
    }
  }

  if (!isValid) {
    // Create or update group error
    let groupError = groupContainer.querySelector('.group-error');
    if (!groupError) {
      groupError = document.createElement('div');
      groupError.className = 'group-error';
      groupError.setAttribute('role', 'alert');
      groupContainer.appendChild(groupError);

      // Associate with all fields in group
      const errorId = `error-group-${groupName}`;
      groupError.id = errorId;
      groupFields.forEach(field => {
        field.setAttribute('aria-describedby', field.getAttribute('aria-describedby') ? 
          `${field.getAttribute('aria-describedby')} ${errorId}` : errorId);
      });
    }

    groupError.textContent = errorMessage;
    groupFields.forEach(field => field.classList.add('invalid-input'));
  } else {
    // Clear group error
    const groupError = groupContainer.querySelector('.group-error');
    if (groupError) {
      groupError.remove();
    }

    groupFields.forEach(field => {
      if (validateField(field, true)) {
        field.classList.remove('invalid-input');
      }
    });
  }

  return isValid;
}
Enter fullscreen mode Exit fullscreen mode

This approach provides context-specific validation for related fields while maintaining clear connections between inputs and error messages.

Focus Management

Strategic focus management is crucial for accessible forms. I direct users to errors efficiently while providing context about remaining issues.

function focusFirstInvalidField() {
  const firstInvalidField = form.querySelector('[aria-invalid="true"]');
  if (firstInvalidField) {
    firstInvalidField.focus();

    // Scroll field into view if needed
    const rect = firstInvalidField.getBoundingClientRect();
    const isInView = (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= window.innerHeight &&
      rect.right <= window.innerWidth
    );

    if (!isInView) {
      firstInvalidField.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  }
}

function announceValidationErrors() {
  // Create or update the error summary
  let errorSummary = form.querySelector('.error-summary');
  if (!errorSummary) {
    errorSummary = document.createElement('div');
    errorSummary.className = 'error-summary';
    errorSummary.setAttribute('role', 'alert');
    errorSummary.setAttribute('tabindex', '-1');
    form.prepend(errorSummary);
  }

  // Generate error summary content
  const errorCount = validationState.errors.size;
  errorSummary.innerHTML = `
    <h2>Please correct ${errorCount} ${errorCount === 1 ? 'error' : 'errors'} in the form</h2>
    <ul>
      ${Array.from(validationState.errors.entries()).map(([fieldId, message]) => {
        const field = document.getElementById(fieldId);
        const fieldName = field.labels[0]?.textContent || field.name || 'Field';
        return `<li>${fieldName}: ${message}</li>`;
      }).join('')}
    </ul>
  `;

  // Announce to screen readers
  errorSummary.focus();
}
Enter fullscreen mode Exit fullscreen mode

This code moves focus to the first invalid field while providing an error summary for context. The smooth scrolling ensures the field is visible, particularly on mobile devices.

I've implemented these techniques across numerous projects and found they significantly improve form accessibility and user satisfaction. The key is creating a validation system that communicates clearly through multiple channels—visual, programmatic, and assistive.

When building accessible form validation, I always test with keyboard navigation and screen readers to ensure the experience works for everyone. The extra effort pays dividends in user satisfaction and form completion rates.

By focusing on these eight techniques, you can create forms that guide all users toward successful completion rather than frustrating them with unclear validation. The approach respects users' abilities and preferences while maintaining a consistent, accessible experience throughout the form interaction.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)