Skip to main content
Saved

Accessible Forms

Category Accessibility

Level Beginner

Examples

Native web

Create forms with proper labels and announced error messages for all users.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Dynamic Error Announcement
  5. Form-Level Success Messaging
  6. Multiple Description References
  7. Benefits
  8. Tradeoffs
  9. Summary
  10. See also

Problem

Screen reader users land on an unlabeled input with no idea what it’s asking for. 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, and screen readers never announced them. Users would submit and get rejected with no idea why. They’d try again with the same data until they gave up.

Keyboard users run into their own problems: a tab order that skips inputs, or focus indicators they can’t see.

Solution

Every input needs a label: a <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.

The required attribute is what tells screen readers a field is required. A visual asterisk next to the label adds nothing for them, so it gets aria-hidden="true", and a short line at the top of the form explains what the asterisk means for sighted users. When you show your own error messages, novalidate on the <form> disables the browser’s built-in validation bubbles while keeping required in place.

When validation fails, red text on its own isn’t enough, because the error also needs to be announced. role="alert" on an error message makes screen readers speak it immediately. On the field itself, aria-invalid="true" marks it as invalid and aria-describedby connects it to its error message.

Related inputs belong in a <fieldset> with a <legend>. Radio buttons for “Payment method” make no sense individually, and the legend gives screen readers that missing context.

DOM order should match visual order. If CSS moves things around visually, tab order still follows the DOM, and keyboard users end up jumping around the page.

Example

An accessible form with announced errors and grouped inputs:

<form novalidate>
  <p>Fields marked * are required.</p>

  <label for="email">Email address <span aria-hidden="true">*</span></label>
  <input
    id="email"
    type="email"
    aria-describedby="email-error email-hint"
    aria-invalid="true"
    required
  />
  <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-hidden="true">*</span>
  </label>
  <input id="username" type="text" required />

  <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

ARIA can announce validation errors as they appear:

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);
}

document.querySelector('form').addEventListener('submit', handleFormSubmit);
</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 15 characters. Spaces are allowed.</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 more people can use your form.
  • WCAG compliance becomes achievable, since proper labels and error handling are WCAG requirements.
  • Clear labels and immediate feedback help every user, and keyboard navigation works properly.

Tradeoffs

  • Your HTML gets more verbose. IDs and aria-describedby references add markup to maintain.
  • Wrong ARIA is worse than no ARIA. If your aria-describedby references a non-existent ID, screen readers have nothing to announce, so you should test your work.
  • Automated checkers only catch obvious issues; you need to test with real screen readers 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-required and HTML required can cause duplicate announcements.

Summary

Accessible forms require proper label associations and meaningful error messages. ARIA handles errors that appear during validation, and a logical tab order makes the form usable without a mouse. The extra markup gets you WCAG compliance and a form that’s easier for everyone to use.

See also

Newsletter

A monthly email
from Den Odell.

Behind-the-scenes thinking on frontend patterns, site updates, and more.

No spam. Unsubscribe anytime.