
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