Accessible Forms
Problem
“Enter your…” what? Screen reader users land on an unlabeled input with no idea what you want. The placeholder says “Email” but placeholders often don’t get announced.
I’ve tested forms where validation errors appeared as red text next to the input, completely invisible to screen readers. Users would submit, get rejected, and have no idea why. They’d try again with the same data, get the same rejection, and eventually give up.
Keyboard users face their own nightmares: Tab skipping inputs randomly, invisible focus indicators, and submit buttons unreachable without a mouse.
Solution
Every input needs a label. Use the <label> element with a for attribute matching the input’s id. Clicking the label moves focus to the input, and screen readers announce the label when users land on the field.
When validation fails, don’t just show red text. Announce it. Use role="alert" on error messages so screen readers speak them immediately, mark invalid fields with aria-invalid="true", and connect them to error messages using aria-describedby.
Group related inputs with <fieldset> and <legend>. Radio buttons for “Payment method” make no sense individually; the legend provides the context screen readers need.
Keep DOM order matching visual order. If CSS moves things around but tab order is chaotic, keyboard users will struggle.
Example
An accessible form with proper labels, announced errors, and grouped inputs:
<form>
<label for="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-error email-hint"
aria-invalid="true"
aria-required="true"
/>
<div id="email-hint">We'll never share your email</div>
<div id="email-error" role="alert">Please enter a valid email address</div>
<label for="username">
Username <span aria-label="required">*</span>
</label>
<input id="username" type="text" aria-required="true" />
<fieldset>
<legend>Newsletter frequency</legend>
<label><input type="radio" name="frequency" value="daily" /> Daily</label>
<label><input type="radio" name="frequency" value="weekly" checked /> Weekly</label>
<label><input type="radio" name="frequency" value="monthly" /> Monthly</label>
</fieldset>
<label>
<input type="checkbox" aria-describedby="terms-error" />
I agree to the terms and conditions
</label>
<div id="terms-error" role="alert"></div>
<button type="submit">Create account</button>
</form>
Dynamic Error Announcement
Announce validation errors dynamically using ARIA:
function validateEmail(input) {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value);
const errorEl = document.getElementById(`${input.id}-error`);
input.setAttribute('aria-invalid', !isValid);
errorEl.textContent = isValid ? '' : 'Please enter a valid email address';
}
document.getElementById('email').addEventListener('blur', function() {
validateEmail(this);
});
Form-Level Success Messaging
A live region announces submission success to screen reader users:
<div role="status" aria-live="polite" aria-atomic="true" id="form-status"></div>
<script>
function handleFormSubmit(event) {
event.preventDefault();
document.getElementById('form-status').textContent =
'Account created successfully. Redirecting...';
setTimeout(() => { window.location.href = '/dashboard'; }, 2000);
}
</script>
Multiple Description References
A single input can reference multiple descriptions for hints, requirements, and errors:
<label for="password">Password</label>
<input
id="password"
type="password"
aria-describedby="password-hint password-requirements password-error"
/>
<div id="password-hint">Choose a strong password</div>
<div id="password-requirements">At least 8 characters, one uppercase, one number</div>
<div id="password-error" role="alert"></div>
Benefits
- Screen reader users know what to enter. Labels tell them exactly what each field expects, and error messages get announced rather than just displayed.
- Form completion rates go up when people can actually use your form.
- WCAG compliance becomes achievable since proper labels and error handling are requirements, not nice-to-haves.
- Everyone benefits: clear labels and immediate feedback improve the experience for all users, and keyboard navigation actually works.
Tradeoffs
- HTML gets more verbose. Labels, IDs, and
aria-describedbyreferences add markup to maintain. - Wrong ARIA is worse than no ARIA. Point
aria-describedbyat a non-existent ID and screen readers get confused, so test your work. - Automated checkers only catch obvious issues; you need actual screen reader testing since NVDA, JAWS, and VoiceOver all behave differently.
- CSS frameworks may conflict with your structure, expecting HTML patterns that don’t match accessible form markup.
- Too many live region announcements overwhelm users. If every keystroke triggers an announcement, you’ve created a new accessibility problem.
- Some JavaScript frameworks break label-input associations by generating dynamic IDs. Using both
aria-requiredand HTMLrequiredcan cause duplicate announcements.
Summary
Accessible forms require proper label associations, meaningful error messages, and logical tab order. Connect inputs to labels, use ARIA for dynamic validation, and ensure keyboard operability. The effort pays off in both compliance and better usability for everyone.