Clean Markup

Accessible Form Error Handling with Live Regions and aria-describedby

Accessible Form Error Handling with Live Regions and aria-describedby

When client-side form validation triggers upon user submission or field blur, visual styling changes—such as crimson borders, warning icons, and error labels beneath fields—provide immediate visual cues to sighted users. However, for screen reader users and individuals navigating via keyboard or assistive technology, these purely visual transformations are completely silent unless accompanied by programmatic accessibility semantics. Without proper ARIA associations and focus orchestration, users may submit a form, encounter silence, and assume their submission succeeded while remaining stranded on an unsubmitted page.

Building an accessible form error experience requires an intentional multi-layered communication strategy conforming to WCAG 2.2 guidelines. By combining a top-level error summary banner announced dynamically through role="alert" or aria-live="assertive", linking specific error messages to form controls via aria-describedby, declaring invalid states with aria-invalid="true", and programmatically moving DOM focus to the summary or the first failing input control, developers ensure every user receives clear, immediate feedback. This pattern bridges visual error presentation with assistive speech output and keyboard navigation seamlessly.

Example

<!-- Form with an Error Summary Banner and linked input fields -->
<form id="signup-form" novalidate>
  <div id="error-summary" class="error-summary" role="alert" tabindex="-1" hidden>
    <h2 id="summary-title">There are errors in your submission</h2>
    <ul id="error-list"></ul>
  </div>

  <div class="field-group">
    <label for="user-email">Email Address</label>
    <input 
      id="user-email" 
      name="email" 
      type="email" 
      aria-describedby="email-error"
      aria-invalid="false"
      required
    />
    <p id="email-error" class="field-error" hidden></p>
  </div>

  <button type="submit">Complete Registration</button>
</form>

Implementation

// Accessible Form Validation Controller
const form = document.getElementById('signup-form');
const summary = document.getElementById('error-summary');
const errorList = document.getElementById('error-list');

form.addEventListener('submit', (event) => {
  event.preventDefault();
  
  // Clear previous error state
  errorList.innerHTML = '';
  summary.hidden = true;
  
  const errors = [];
  const fields = form.querySelectorAll('input[required]');

  fields.forEach((field) => {
    const errorEl = document.getElementById(`${field.id}-error`);
    
    // Evaluate field validity
    if (!field.validity.valid) {
      const message = field.validity.valueMissing
        ? `${getLabelText(field)} is required.`
        : `Please enter a valid ${getLabelText(field).toLowerCase()}.`;

      // 1. Set invalid state and expose description link
      field.setAttribute('aria-invalid', 'true');
      if (errorEl) {
        errorEl.textContent = message;
        errorEl.hidden = false;
      }

      errors.push({ field, message });
    } else {
      // 2. Clear invalid state for valid inputs
      field.removeAttribute('aria-invalid');
      if (errorEl) {
        errorEl.textContent = '';
        errorEl.hidden = true;
      }
    }
  });

  // 3. Handle errors and manage keyboard focus
  if (errors.length > 0) {
    errors.forEach(({ field, message }) => {
      const li = document.createElement('li');
      const link = document.createElement('a');
      link.href = `#${field.id}`;
      link.textContent = message;
      
      // Focus the invalid field when clicking summary item
      link.addEventListener('click', (e) => {
        e.preventDefault();
        field.focus();
      });

      li.appendChild(link);
      errorList.appendChild(li);
    });

    // Make live region visible and move focus to first error or summary
    summary.hidden = false;
    summary.focus();
  } else {
    // Form submission proceeds successfully
    summary.hidden = true;
    form.submit();
  }
});

function getLabelText(field) {
  const label = document.querySelector(`label[for="${field.id}"]`);
  return label ? label.textContent.trim() : field.name;
}

Main Point: Robust form accessibility pairs aria-invalid and aria-describedby with an assertive live error summary banner and intentional programmatic focus management.

Credits

Photo by Scott Webb on Unsplash

← Home