Building Accessible Forms: The Stuff Most Tutorials Skip


Every web development tutorial teaches you how to build a form. Most of them get you about 60% of the way to a good form and then stop. They cover <input>, <label>, <button>, maybe some basic validation. Then they move on.

What they don’t cover is the stuff that makes a form actually usable for everyone - people using screen readers, keyboard-only users, people with cognitive disabilities, and frankly, everyone else too. Good accessibility makes forms better for all users, not just those with disabilities.

I learned this the hard way when an accessibility audit on a project I’d built came back with a long list of form issues. I’d done all the “basic” stuff. Labels were there. Inputs had types. Buttons were buttons. But the form was still a poor experience for screen reader users, and the error handling was basically invisible to anyone not using a mouse.

Here’s what I’ve learned since then.

Labels: The Thing Everyone Gets Half Right

Yes, every input needs a label. Most tutorials cover this. But there are nuances:

Always use explicit <label> elements with for attributes. Not placeholder text as labels. Not aria-label as a substitute for visible labels. A visible <label> element with a for attribute matching the input’s id.

<!-- Good -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" />

<!-- Bad - placeholder as label -->
<input type="email" placeholder="Email address" />

Placeholder text disappears when you start typing. If you tab away and come back, you might not remember what the field was for. Screen readers don’t consistently announce placeholder text the way they announce labels.

Group related inputs with <fieldset> and <legend>. Radio buttons, checkboxes, and multi-field inputs (like date of birth with separate day/month/year fields) should be wrapped in a <fieldset> with a <legend> that describes the group.

<fieldset>
  <legend>Preferred contact method</legend>
  <label><input type="radio" name="contact" value="email" /> Email</label>
  <label><input type="radio" name="contact" value="phone" /> Phone</label>
</fieldset>

Without the fieldset/legend, a screen reader user tabbing through radio buttons hears “Email, radio button” and “Phone, radio button” without context about what they’re choosing between.

Error Handling: Where Most Forms Fail

Error handling is where form accessibility falls apart most often. The typical approach - show a red message next to the field - works for sighted mouse users and nobody else.

Associate error messages with inputs using aria-describedby:

<label for="email">Email address</label>
<input
  type="email"
  id="email"
  aria-describedby="email-error"
  aria-invalid="true"
/>
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

The aria-describedby attribute tells screen readers to announce the error message when the input receives focus. The aria-invalid="true" attribute signals that the current value is invalid. The role="alert" ensures the error is announced immediately when it appears.

Move focus to the first error on submission. When a form is submitted with errors, move keyboard focus to the first field with an error. Without this, keyboard and screen reader users are left wondering what happened after they pressed submit.

const firstError = document.querySelector('[aria-invalid="true"]');
if (firstError) {
  firstError.focus();
}

Provide an error summary at the top of the form. For forms with multiple errors, add a summary at the top listing all errors with links to the relevant fields. The GOV.UK Design System has an excellent implementation of this pattern.

<div role="alert" aria-labelledby="error-summary-title">
  <h2 id="error-summary-title">There are 2 errors in this form</h2>
  <ul>
    <li><a href="#email">Enter a valid email address</a></li>
    <li><a href="#password">Password must be at least 8 characters</a></li>
  </ul>
</div>

Focus Management

Focus management is invisible to mouse users but critical for keyboard users.

Never remove the focus outline. outline: none on inputs and buttons is one of the most harmful CSS patterns in web development. The focus outline tells keyboard users where they are on the page. Removing it is like removing the mouse cursor for mouse users.

If you don’t like the browser’s default outline, style it differently. Don’t remove it.

/* Bad */
input:focus { outline: none; }

/* Good */
input:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Manage focus when the form state changes. If submitting a form shows a success message, move focus to that message. If a form step changes (multi-step forms), move focus to the new step’s heading. If a modal with a form opens, move focus into the modal and trap it there until it closes.

Tab order should follow visual order. Don’t use tabindex values greater than 0. They create a custom tab order that’s almost impossible to maintain and usually confusing. If your tab order doesn’t match the visual order, fix the DOM order or CSS layout, not the tabindex.

Live Regions for Dynamic Content

If your form updates content dynamically (loading states, character counters, real-time validation), use ARIA live regions to announce changes to screen readers.

<input type="text" maxlength="200" aria-describedby="char-count" />
<span id="char-count" aria-live="polite">200 characters remaining</span>

aria-live="polite" means the screen reader will announce the update at the next convenient pause. Use aria-live="assertive" for urgent updates (errors), but sparingly - interrupting the user constantly is annoying.

Testing Your Forms

Accessibility testing doesn’t require specialised equipment. Here’s what I do:

  1. Tab through the entire form using only the keyboard. Can you reach every field, select every option, and submit the form without touching the mouse? Can you see where focus is at all times?

  2. Use a screen reader. VoiceOver on Mac (Command + F5 to toggle) or NVDA on Windows (free). Navigate through your form and listen to what’s announced. Is every field identified? Are errors communicated? Does the experience make sense without seeing the screen?

  3. Run automated tools. axe DevTools browser extension catches many common issues. It won’t catch everything (automated tools miss about 40-60% of accessibility issues), but it’s a good starting point.

  4. Zoom to 200%. Does the form still work? Do labels stay associated with their inputs? Does the layout break?

The Payoff

Building accessible forms takes more effort upfront. But the result is a form that’s better for everyone - not just users with disabilities but also power users who prefer keyboards, mobile users with small screens, users with temporary injuries, and users in contexts where their usual interaction methods aren’t available.

Accessibility isn’t a feature you add at the end. It’s a quality standard you build to from the start. And forms, being the primary way users interact with web applications, are where it matters most.