Forms are where users sign up, pay, contact support, and complete the tasks your product depends on. They are also one of the most common sources of accessibility failures on the Web. WebAIM's Million report found that one third of form inputs on home pages lack a proper label, and 51% of home pages have missing form input labels overall — a figure that ticked upward in the latest analysis as forms grow more complex.
There are quite a few moving parts when building out accessible forms, but they boil down to a clear sequence: name every field, connect related information programmatically, communicate state changes to assistive technologies, and prefer native HTML before reaching for custom widgets.This guide walks through that sequence step by step so you can apply it on the next form you ship out to the world.
If you have not read our overview of broader accessibility principles, start with Web Accessibility Best Practices for Developers. This post goes deep on forms specifically.
TL;DR
30-second summary
What does it take to build a form that works correctly for keyboard and screen reader users, not just sighted mouse users?
- Every form input needs a programmatically associated label, not just visible text nearby. A
<label>connected via theforattribute, or a wrapped label, ensures screen readers announce the correct name when a user focuses the input. Placeholder text is not a substitute for a label. It disappears once text is entered and is not reliably announced by all assistive technology. - Grouping related inputs requires semantic structure, not just visual layout. A
<fieldset>with a<legend>groups radio buttons or checkboxes so screen reader users hear the group's purpose before hearing each option, which visual proximity alone cannot communicate. - Error messages need to be programmatically linked to their input and announced when they appear. Using
aria-describedbyto connect an error message to its field, combined witharia-invalid="true", ensures screen readers announce the problem at the moment of failure. Errors should also be visible in text, not conveyed through color alone. - Required fields need more than a visual asterisk. The
requiredattribute oraria-required="true"communicates the requirement to assistive technology directly, rather than relying on a visual convention that a screen reader user has no way to perceive. - Autocomplete attributes and correct input types reduce cognitive and motor effort for every user. Using
autocompletevalues and appropriateinputmodeandtypeattributes lets browsers and assistive technology offer relevant suggestions and keyboards, reducing the effort needed to complete a form. This is a benefit that helps users with motor and cognitive disabilities most, but improves the experience for everyone.
Bottom line: A form is often the single most consequential accessibility surface on a website, because failing to complete it means failing to complete the task the entire page exists for. Programmatic labels, semantic grouping, linked error messages, and thoughtful input attributes are what separate a form that looks fine to a sighted user from one that actually works for everyone.
Label every input
Every form input needs a name. Screen readers announce that name when the input receives focus. Sighted users rely on visible labels (or surrounding context) to understand what to enter. Without a label, both groups are left guessing.
The <label> element is our primary tool. Connect it to an input in one of two ways:
Explicit labeling uses the for attribute on the label and a matching id on the input. Reach for this when the label and input are separated in the layout or the DOM:
<label for="first-name">First name</label>
<input id="first-name" type="text" />Implicit labeling wraps the input inside the label. This is a super simple way to label your inputs and it works well when the label and input are close together:
<label>
First name
<input type="text" />
</label>When a visible label is truly not feasible — spreadsheet cells, browser URL bars, quick filters where surrounding context makes the purpose obvious — use invisible labels via aria-label or aria-labelledby. Reserve these for cases where visible text would clutter the UI, but don't just use it as a shortcut to avoid design work.
One rule worth treating as a non-negotiable: never rely on a placeholder as the label. Placeholders disappear once the user starts typing, often fail contrast requirements and can trick automated scanners into reporting a false pass.Always provide a real label. The WCAG success criterion 3.3.2 Labels or Instructions requires that labels or instructions are provided when user input is required.
Group related fields
Address blocks, payment sections, and preference panels often contain several inputs that belong together. Users need to hear the group name, "Shipping address", before individual field names like "Street" and "ZIP code."The classic approach wraps related controls in a <fieldset> element with a <legend>:
<fieldset>
<legend>Shipping address</legend>
<label for="shipping-street">Street</label>
<input type="text" id="shipping-street" />
<label for="zip-code">ZIP code</label>
<input type="text" id="zip-code" inputmode="numeric" />
</fieldset>If <fieldset> styling fights your design system, use a container with role="group" and add a group label via aria-labelledby:
<div role="group" aria-labelledby="shipping-addr">
<p id="shipping-addr">Shipping address</p>
<!-- individual labeled inputs -->
</div>Both approaches work fine, so pick the one that fits your layout without sacrificing the group relationship in the Accessibility Tree. Grouping related controls supports the 1.3.1 Info and Relationships success criterion, which requires that information and structure conveyed visually are also available programmatically.
Hints and descriptions
Some inputs need more than a label. A password input might require "At least 12 characters, a date input might need a specific format. These constraints can be considered as additional descriptions, and they must be programmatically linked to their input using aria-describedby:
<label for="password">Password</label>
<input type="password" id="password" aria-describedby="password-hint" />
<span id="password-hint">Must be at least 12 characters long.</span>Screen readers read the label first, then the description, when focus lands on the input.You can also reference multiple IDs in aria-describedby, separate them with spaces, to build compound descriptions.
Errors & live regions
Validation errors are a crucial part of forms, but they appear dynamically and are not initially present on the page. When they are present, sighted users will see red text appear beside a field, but screen reader users will hear nothing unless we tell the browser to announce the change. That is what live regions are for.
Mark a container with aria-live="polite" for non-urgent updates like validation errors, or use role="alert" when the message needs immediate attention. Dynamic status messages should meet the 4.1.3 Status Messages success criterion. Place error text inside the live region and reference it from the input:
<label for="pwd">Password</label>
<input type="password" id="pwd" aria-describedby="error-msg hint-msg" aria-invalid="true" />
<span id="hint-msg">Must be at least 12 characters long.</span>
<span aria-live="polite" id="error-msg">Password is too short.</span>Notice the order of the compound description aria-describedby="error-msg hint-msg". It is generally considered a good idea to put the error message first and then the hint message, so that screen readers will first read out the problem, followed by a potential way on how to solve it.
Error patterns
We have two common patterns for surfacing errors:
Inline errors sit next to the input they describe. They work best after form submission, when the user can read errors from top to bottom without interruption. Showing errors on every keystroke or on blur tends to distract and confuse folks, so save inline validation for submit time unless you have a strong UX research that tells you otherwise.
Summary errors appear at the top of the form in a global live region, listing everything that failed. Enhance this pattern by moving focus to the summary (tabindex="-1") and wrapping each error in a link that jumps to the offending field:
<div role="alert" tabindex="-1">
<p>There was a problem sending your message.</p>
<ul>
<li><a href="#first-name">First name cannot be empty</a></li>
<li><a href="#last-name">Last name cannot be empty</a></li>
</ul>
</div>Regardless of the pattern, you should never rely on color alone to signal an error. Pair red text with an icon and clear, actionable language. "Invalid input" helps nobody, whereas "Password must be at least 12 characters" tells the user exactly what to do. The WCAG 3.3.1 Error Identification success criterion requires that input errors are identified and described to the user in text.
Does your form actually announce errors to screen reader users, or just show them visually?
Our accessibility testing services verify forms against real assistive technology, not just automated scanners that miss context-dependent issues like this.
Required and invalid states
Required inputs should be announced as such. On native inputs, the required attribute does this automatically. Screen readers will say "Name, required, edit text." For custom components like a styled dropdown, use aria-required="true" instead. Note that aria-required communicates state but does not enforce validation, meaning the browser won't yell at you if you submit the form with the custom styled dropdown being empty. You still need your own validation logic there.
After validation runs, set aria-invalid="true" on fields that failed and aria-invalid="false" on fields that passed. This gives assistive technology users immediate feedback about which inputs need attention.
When you implement custom validation in JavaScript, add novalidate to the <form> element to disable inconsistent browser-native error UI and handle messaging yourself.
Think of the full submission lifecycle
Accessible forms account for every state change. This includes submission, loading, and success states.
Submission
Provide at least two ways to submit the form: a <button type="submit"> inside the form, and Enter-key submission from any single-line input. If the submit button must live outside the form markup, connect it with the form attribute pointing to the form's id.
<form id="contact-form">
<input type="email" name="email" placeholder="Email" />
</form>
<button type="submit" form="contact-form">Submit</button>Loading
Disable the submit button (and ideally all inputs) to prevent duplicate submissions. Wrap loading indicators in a live region with visible or visually hidden text — a spinner alone is invisible to screen readers:
<div aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<span class="sr-only">Loading…</span>
</div>For skeleton placeholders, set aria-busy="true" and an aria-label describing what is loading until content arrives.
Success
This is the most overlooked step. When submission succeeds, announce it through a live region, even if the success message is visually subtle. Users who cannot see a green checkmark still need confirmation that their action worked.
Improving UX
Once your accessibility foundation is solid, a few extra HTML attributes will make your forms faster and easier to complete, and that's a win-win for everyone.
The autocomplete attribute tells browsers and password managers what each input represents. Correct values reduce errors, speed up completion for users with motor or cognitive disabilities, and give Screen Readers richer context. Set it on name, email, address, and payment inputs. Turn it off (autocomplete="off") for one-time codes, promo codes, and fields where autofill would be a security or UX problem. CVV numbers are a common example due to PCI compliance rules.
Choose the right type (email, tel, password, number) so browsers provide appropriate validation and mobile keyboards. When type="number" spinners get in the way, combine type="text" with inputmode="numeric" for digit-only fields like ZIP codes.
| Attribute | What it does | Example use |
|---|---|---|
| required | Marks native inputs as mandatory; announced by Screen Readers | <input required type="text"> |
| aria-required | Same announcement for custom components | <custom-select aria-required="true"> |
| aria-invalid | Signals validation result after submit | aria-invalid="true" on failed fields |
| autocomplete | Enables browser autofill and richer AT context | autocomplete="email" |
| inputmode | Controls mobile keyboard layout | inputmode="numeric" for ZIP codes |
Putting it all together
Accessible forms come down to a handful of habits we can verify on every PR:
- Every input has a visible label (or a deliberate invisible alternative)
- Hints and errors are linked with
aria-describedby - Dynamic messages, errors, loading, success, live inside live regions
- Required and invalid states are communicated with required,
aria-required, andaria-invalid - Native HTML comes first - custom widgets get the
ARIAthey need
Web accessibility is context-driven. It depends on the situation and the UI we are aiming to create and there are no silver bullet solutions that apply to every situation always. The steps above give us a reliable checklist to work from.Apply them to a form in your codebase today. You may be surprised how many gaps you find once you test with a keyboard and a screen reader.
FAQ
Most common questions
Why isn't placeholder text a substitute for a form label?
Placeholder text disappears the moment a user starts typing, which means there's no persistent visual reminder of what the field is for once it contains content. It's also not reliably announced by all screen readers, and even when it is, some browsers don't expose placeholder text with sufficient contrast for low-vision users. A programmatically associated <label>, connected using the for attribute or by wrapping the input, ensures the field's name is always available to assistive technology and remains visible for sighted users regardless of what's been typed.
How should related form inputs like radio buttons be grouped for accessibility?
Related inputs should be wrapped in a <fieldset> with a <legend> describing the group's purpose. This ensures a screen reader announces the group's overall context, such as "Shipping method," before announcing each individual option inside it. Without this structure, a screen reader user hears each radio button's label in isolation, with no indication of what they collectively represent, even though a sighted user can infer the relationship from visual proximity alone.
How should form errors be communicated accessibly?
Error messages should be programmatically linked to their corresponding input using aria-describedby, and the input itself should carry aria-invalid="true" when validation fails. This ensures a screen reader announces the specific error at the moment a user encounters it, rather than leaving them to guess why submission failed. Errors also need to be conveyed through visible text, not through color alone, since color-only indicators are invisible to users with low vision or color vision deficiencies.
Why is the required attribute important beyond a visual asterisk?
A visual asterisk next to a label is a sighted-user convention that communicates nothing to assistive technology on its own. Using the native required attribute, or aria-required="true" where native HTML isn't sufficient, tells screen readers and other assistive technology directly that a field must be completed before submission. This ensures the requirement is announced as part of the field's accessible description, not just implied visually.
How do autocomplete and input type attributes improve form accessibility?
Setting appropriate autocomplete values allows browsers to offer relevant autofill suggestions, reducing the typing and recall effort required to complete a form. Using the correct type and inputmode attributes, such as type="email" or inputmode="numeric", triggers the appropriate on-screen keyboard on mobile devices and enables browser-level validation. These attributes disproportionately help users with motor or cognitive disabilities who benefit most from reduced typing effort, while also improving completion speed and accuracy for every user.
A form nobody can complete is a form that doesn't work
We help engineering teams test forms with real screen readers, keyboard-only navigation, and the WCAG criteria that automated tools consistently miss.





