A Complete Guide to Accessible Forms
Think about everything your users do through forms: log in, sign up, check out, contact you. If those forms aren't accessible, you've essentially locked people out of your site. And I'm not exaggerating: missing labels are one of the top three WCAG violations I find in scans. Let me show you how to get this right.
In This Article
Labels: The Foundation
Every input needs a label. I can't stress this enough. Without one, screen reader users land on your input and hear "edit text," with absolutely no idea what they're supposed to type. That's a terrible experience.
The for/id Pattern
This is the most reliable approach, and the one I'd recommend:
<label for="email">Email address</label>
<input type="email" id="email" name="email">
The for attribute on the label has to match the id on the input exactly. This gives screen readers the connection they need, and it also makes the label clickable, so clicking "Email address" focuses the input. That's a win for everyone, not just assistive tech users.
Wrapping Pattern
You can also just wrap the input inside the label:
<label>
Email address
<input type="email" name="email">
</label>
This works fine in modern browsers, but I still prefer the explicit for/id approach above. It's more reliable across different assistive technologies.
When You Can't Use a Visible Label
Sometimes your designer wants a clean search box with no visible label, just a magnifying glass icon. I get it. But you still need to give assistive technology something to work with:
<!-- Option 1: aria-label -->
<input type="search" aria-label="Search this site">
<!-- Option 2: visually hidden label (preferred) -->
<label for="search" class="visually-hidden">Search this site</label>
<input type="search" id="search">
WCAG References
SC 1.3.1 (Info and Relationships) requires that labels are programmatically associated with their inputs. SC 3.3.2 (Labels or Instructions) requires that labels or instructions are provided when user input is needed. If you're new to these standards, my intro to WCAG explains how the success criteria work.
Missing labels are, by a wide margin, the single most common form accessibility failure I encounter. I've scanned thousands of sites and it shows up almost every time. The fix is usually straightforward: add a <label>, match the for and id, done. If you're dealing with this on your own site, I wrote a step-by-step walkthrough on how to fix missing form labels that covers every scenario, from simple text inputs to file uploads and custom dropdowns.
Grouping Related Fields
When you've got related inputs (radio buttons, checkboxes, an address block), you need to wrap them in a <fieldset> with a <legend>. Without this, screen reader users lose important context:
<fieldset>
<legend>Shipping method</legend>
<label>
<input type="radio" name="shipping" value="standard">
Standard (5-7 days)
</label>
<label>
<input type="radio" name="shipping" value="express">
Express (2-3 days)
</label>
<label>
<input type="radio" name="shipping" value="overnight">
Overnight
</label>
</fieldset>
The screen reader announces the legend when someone enters the fieldset. Without it, a user just hears "radio button, Standard 5-7 days" and has no idea what "Standard" even refers to. Standard what?
When to Use Fieldsets
- Radio button groups (always)
- Checkbox groups with a shared question
- Related fields like "Billing Address" or "Date of Birth"
- Multi-step forms where each step is a logical group
Accessible Validation and Errors
This is where I see a lot of sites fall apart. Something goes wrong with the form, and the user has no idea what happened, which field is the problem, or how to fix it. Let's do better.
Required Fields
Don't make people guess which fields are required. Mark them clearly:
<label for="name">
Full name <span aria-hidden="true">*</span>
</label>
<input type="text" id="name" required aria-required="true">
The required attribute provides native browser validation and screen reader announcements. The visible asterisk helps sighted users, and aria-hidden="true" prevents screen readers from announcing "star."
Error Messages
When validation fails, errors must be:
- Visible: Displayed in text near the field (not just a red border)
- Descriptive: Explain the problem and how to fix it
- Programmatically linked: Connected to the field via
aria-describedby - Announced: Screen readers should be notified of the error
<label for="email">Email address</label>
<input type="email" id="email"
aria-describedby="email-error"
aria-invalid="true">
<p id="email-error" class="error" role="alert">
Please enter a valid email address (e.g., [email protected])
</p>
The aria-invalid="true" tells screen readers the field has an error. The aria-describedby links the error message to the field. The role="alert" ensures the error is announced immediately when it appears.
WCAG References
SC 3.3.1 (Error Identification) requires that errors are identified and described in text. SC 3.3.3 (Error Suggestion) requires that you suggest corrections when you can detect them.
Error Summaries
If the form has multiple errors, don't just highlight each field. Show a summary at the top. I like to link each item directly to its field so users can jump right to the problem:
<div role="alert" class="error-summary">
<h2>Please fix the following errors:</h2>
<ul>
<li><a href="#email">Email address is required</a></li>
<li><a href="#password">Password must be at least 8 characters</a></li>
</ul>
</div>
The Placeholder Problem
I need to be blunt about this: placeholder text is not a label. I see this mistake everywhere, and here's why it doesn't work:
- It disappears when users start typing, removing the only instruction about what to enter
- Low contrast: Default placeholder text is typically light gray, failing contrast requirements
- Inconsistent support: Screen readers handle placeholders differently; some announce them, some don't
- Cognitive load: Users with memory difficulties can't recall what vanished placeholder said
- Confusion: Users may think a field is already filled in because it has text
<!-- Bad: placeholder as label -->
<input type="email" placeholder="Email address">
<!-- Good: real label + helpful placeholder -->
<label for="email">Email address</label>
<input type="email" id="email" placeholder="[email protected]">
If you want to use a placeholder, that's fine, but just make it a hint (like showing the expected format), not the only label the field has.
Complex Form Patterns
Autocomplete Attributes
Here's a quick win you might not know about: WCAG 2.2 actually requires you to add autocomplete attributes on fields that collect personal info. It helps browsers and assistive tech pre-fill data, and it's easy to do:
<input type="text" id="name" autocomplete="name">
<input type="email" id="email" autocomplete="email">
<input type="tel" id="phone" autocomplete="tel">
<input type="text" id="address" autocomplete="street-address">
Custom Form Controls and ARIA
Standard HTML inputs come with built-in accessibility for free. But once you start building custom dropdowns, date pickers, or toggle switches, you're on your own. The browser won't announce these correctly unless you add the right ARIA roles and properties. A custom dropdown needs role="listbox" on the container, role="option" on each item, and proper aria-expanded and aria-activedescendant management. It's a lot of work, and it's easy to get wrong. My ARIA labels guide covers the patterns you'll need. Honestly, if you can stick with native HTML elements, do it. A styled <select> is almost always better than a custom dropdown built from <div> elements.
Multi-Step Forms
If you're splitting a long form across multiple steps, here's what you need to keep in mind:
- Indicate progress (e.g., "Step 2 of 4")
- Allow navigating back to previous steps
- Preserve entered data when navigating between steps
- Use
aria-current="step"on the current step indicator
Dynamic Form Fields
If fields show up or disappear based on what the user selects, you've got to announce those changes. Otherwise screen reader users won't know the form just changed on them. Use aria-live regions:
<div aria-live="polite">
<!-- New fields inserted here will be announced -->
</div>
Quick Checklist
Here's what I check on every form I review. Run through this for yours:
- Every input has a visible, associated
<label> - Required fields are marked with
requiredandaria-required="true" - Related fields are grouped with
<fieldset>and<legend> - Error messages identify the field and describe the fix
- Errors are linked to fields via
aria-describedby - Invalid fields use
aria-invalid="true" - Placeholders supplement labels, never replace them
- Personal data fields have
autocompleteattributes - The form works entirely by keyboard
- Submit buttons have clear, descriptive text
Scan Your Forms for Issues
Want to know how your forms are doing right now? Run a free scan and I'll catch missing labels, empty form controls, and other issues automatically. For the full picture, check out the ADA compliance checklist, and make sure your forms are keyboard accessible too.