Why Forms Fail Accessibility
Forms are where accessibility problems hurt the most. A low-contrast paragraph is frustrating. A broken form is a dead end. If a user cannot complete a checkout, submit an application, or create an account, the rest of your product doesn't matter. They're out.
The WebAIM Million, an annual accessibility audit of the top one million homepages, consistently finds that form inputs with missing labels are among the five most common WCAG failures, year after year. And that's just the homepage. Inside products, behind logins, the situation is usually worse.
What makes forms hard isn't that the requirements are complicated. Most of the failures are mundane: a label that floats away on focus and leaves a screen reader user with no context, an error message that appears visually but isn't announced, a group of radio buttons that make no sense without knowing what question they're answering. These are design decisions, not implementation gaps. They happen in Figma before a developer writes a single line.
This article covers the five form design decisions that matter most for accessibility: field grouping, label placement, error messaging patterns, inline versus summary error strategies, autocomplete attributes, and the required versus optional convention debate. Get these right, and most of your form accessibility work is done.
Field Grouping with Fieldset and Legend
When sighted users look at a form with three fields (First Name, Last Name, Date of Birth), the visual layout tells them how those fields relate. The proximity, the heading above, the visual grouping: it's all self-evident. Screen reader users don't have that. They navigate field by field, hearing only the label for each input in sequence.
This is where <fieldset> and <legend> earn their keep. A <fieldset> groups related fields semantically, and the <legend> provides the group's name. Screen readers announce both together: "Personal Information group, First Name." Without this, a user hears "First Name" with no context about which section of the form they're in.
The most critical use case is radio buttons and checkboxes. A set of radio buttons with labels like "Yes" and "No" is meaningless without context. The question itself, "Do you want to receive marketing emails?", must be the legend of the fieldset. Without it, a screen reader user hears "Yes" and has no idea what they're agreeing to.
<fieldset>
<legend>Do you want to receive marketing emails?</legend>
<label>
<input type="radio" name="marketing" value="yes"> Yes
</label>
<label>
<input type="radio" name="marketing" value="no"> No
</label>
</fieldset>
Fieldsets aren't just for radio buttons. Address blocks, payment card details, date of birth fields split across day/month/year inputs. Any logical grouping of fields that belongs together conceptually should be wrapped in a fieldset with a meaningful legend. The visual styling is up to you; the <fieldset> border can be reset to none in CSS without losing the semantic benefit.
If your design uses a visual heading above a group of fields (like "Billing Address"), the developer needs to know that this heading is also the legend for those fields, not a decorative heading above a plain <div>. Annotate your Figma files with this distinction. It's not obvious without the instruction.
Labels: Placement, Persistence, and Association
Every form field needs a visible, persistent label. This seems obvious. It isn't obvious enough, because placeholder text is still being used as a substitute for labels on live products every single day.
Placeholders are not labels. When a user focuses a field, the placeholder disappears. At the exact moment the user is trying to fill in the field, the prompt that told them what to enter is gone. This is especially harmful for users with cognitive disabilities, short-term memory difficulties, or anyone filling in a long form. By the time they're three fields in, they've lost context on the first.
Placeholders also fail contrast requirements. WCAG requires 4.5:1 contrast for text. Placeholder text is typically styled at far lower contrast by browser default, and even when designers increase it, the lighter styling signals "optional hint," not "required field label." Many users skip fields they think are already filled in because the placeholder text looks like content.
If you need a hint inside the field, like an example value or a format guide, use it as real hint text below or above the input, not as the placeholder. Reserve the placeholder for genuinely optional example content (like "e.g. 01/01/1990") that supplements a real, visible label. Never use it as a replacement.
Floating labels, where the label starts inside the field and animates above it on focus, look elegant but have real accessibility risks. They only work if the label remains visible after the user types. If the label ever disappears or overlaps entered content, it fails. Test floating labels with a screen magnifier at 200% zoom, where the label animation can take the label off-screen.
Label placement matters for more than aesthetics. Research and eye-tracking studies consistently show that labels placed above their fields perform best: they sit in the natural reading path, they're visible before the user focuses the input, and they don't compete with the input itself. Left-aligned labels work too, but the horizontal distance between label and input creates a scanning gap for users who zoom or use reflow layouts.
On the implementation side, labels must be programmatically associated with their inputs using either a for/id pairing or by wrapping the input inside the label element. A visual label positioned near a field with no semantic connection is invisible to assistive technology.
<!-- Explicit association -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" />
<!-- Implicit association (label wraps input) -->
<label>
Email address
<input type="email" name="email" />
</label>
Error Messaging Patterns
Error messages are one of the most consequential pieces of writing in any form. When something goes wrong, the message is the only thing standing between the user completing the task and abandoning it entirely. And yet they're almost always written last, by whoever is closest to a deadline, with the least amount of user testing.
An accessible error message does five things: it identifies the field in error, explains what went wrong, says what format or value is expected, avoids blame, and tells the user exactly how to fix it. "Invalid input" does none of these. "Enter your date of birth in DD/MM/YYYY format" does all of them.
From a WCAG perspective (Success Criterion 3.3.1), errors must be identified in text. Color alone is not enough. A red border on an input communicates error visually, but a screen reader won't announce it unless the error is described in text and that text is associated with the field. The visual indicator and the accessible announcement are two separate things, and both are required.
Error messages should be positioned immediately below the field they describe. Not above, not in a sidebar, not at the top of the page and nowhere near the field. The message needs to be adjacent to the problem. Place error messages between the input and the next field so keyboard users navigating linearly encounter them in the right order.
Use aria-describedby to associate the error message with its input. This means when a screen reader user focuses the field, they hear both the label and the error together. Without this, the error message exists in the DOM but has no relationship to the field it's describing.
<label for="dob">Date of birth</label>
<input
type="text"
id="dob"
name="dob"
aria-describedby="dob-error"
aria-invalid="true"
/>
<span id="dob-error" role="alert">
Enter your date of birth in DD/MM/YYYY format
</span>
The aria-invalid="true" attribute signals to assistive technology that the field is in an error state, which changes how screen readers announce the field. Remove it (or set it to "false") once the error is resolved. Leaving aria-invalid="true" on a valid field will confuse users.
Inline Errors vs. Error Summaries
There are two places errors can appear: inline (next to the field that failed) and in a summary (at the top of the form, listing all errors). Both have a role. The question is understanding when each approach works and when it fails.
Inline errors are the right default for most forms. They appear adjacent to the field, they're immediately scannable by sighted users, and when linked via aria-describedby, they're announced when screen reader users focus the invalid field. For short forms, a single page with five to ten fields, inline errors are almost always the better choice. Users can see at a glance what needs fixing without scrolling anywhere.
Error summaries are essential for long or multi-section forms. When a form has twenty fields across multiple sections and a user submits with eight errors, inline messages below each field don't help them understand the scope of the problem or navigate to the specific issues efficiently. An error summary at the top, listing all errors with links to each field, solves this. The user sees the full picture immediately, and each link in the summary jumps them directly to the problem field.
When using a summary, move focus to it on form submission failure. If the page doesn't scroll and focus doesn't move, screen reader users have no way of knowing errors occurred. The summary container should receive programmatic focus (using tabindex="-1" and focus() via JavaScript) so it's announced immediately after submission.
Use both together on long forms: an error summary at the top for orientation and navigation, plus inline messages beside each field for correction. The summary tells users what's wrong and where. The inline message tells them exactly how to fix it. They serve different cognitive purposes.
One more consideration: when to show errors. Validating on submission is the safest default because users have finished expressing their intent before you tell them they're wrong. Validating on blur (when a field loses focus) can work for format-specific fields like email or date of birth, but only if you don't flag an error while the user is still typing. Aggressive inline validation that fires on every keystroke creates noise, interrupts screen reader users mid-entry, and makes forms feel hostile.
Autocomplete Attributes
The autocomplete attribute is one of the most underused accessibility tools in form design. WCAG 1.3.5 (Identify Input Purpose) requires it on fields that collect personal information, not as a nice enhancement, but as a Level AA requirement. And yet most forms are shipped without it.
The purpose of autocomplete is to let browsers and password managers pre-fill fields based on saved user data. For users with motor disabilities or cognitive impairments, filling in a long address form or creating a new account manually is a significant effort. Autocomplete removes that effort by letting the browser do it. For users with dyslexia, it removes the cognitive load of transcribing information they've already given somewhere else.
The attribute takes specific token values defined in the HTML specification. These aren't arbitrary strings; they're a controlled vocabulary that browsers and assistive technologies understand. Common values include:
| Field | autocomplete value |
|---|---|
| First name | given-name |
| Last name | family-name |
| Full name | name |
| Email address | email |
| Phone number | tel |
| Street address | street-address |
| City | address-level2 |
| Postcode / ZIP | postal-code |
| Country | country-name |
| Credit card number | cc-number |
| Current password | current-password |
| New password | new-password |
| One-time code | one-time-code |
Setting autocomplete="off" on personal information fields to "prevent autofill for security reasons" is a pattern worth pushing back on. It breaks assistive technology, forces manual entry, and doesn't actually improve security in the way teams typically assume. The one legitimate use case for disabling autocomplete is OTP fields where you're managing the input yourself, and even then, one-time-code is the right value, not off.
As a designer, your job is to document the intended autocomplete value alongside the field in your handoff. Developers can't guess what token to use for an ambiguous field labelled "Name": is it name, given-name, or something else? The design spec should tell them.
Required vs. Optional: Conventions That Actually Work
The asterisk. Every designer knows it. "Required fields are marked with an asterisk," usually in a note at the top of the form that most users never read, in a font size small enough to miss, placed before a form with forty fields where thirty-eight have asterisks. At that point, what is the asterisk actually communicating?
The GDS (UK Government Design System) guidance on this is the clearest in the industry: if most fields in a form are required, mark the optional ones instead. The asterisk convention breaks down when required is the norm. It adds visual noise, creates cognitive overhead, and is often missed by screen reader users unless it's explicitly described.
For accessible implementation, the asterisk must be supplemented by text. A screen reader doesn't announce "asterisk" as "required" by default. The field's accessible name or the associated description needs to explicitly include the word "required," either in the label itself, in an aria-required="true" attribute, or via an associated description that says so.
<!-- The asterisk is decorative; aria-required carries the semantic meaning -->
<label for="email">
Email address <span aria-hidden="true">*</span>
</label>
<input type="email" id="email" name="email" aria-required="true" />
Note aria-hidden="true" on the asterisk itself. Without it, some screen readers will announce "asterisk" as part of the label, which is confusing. The aria-required="true" attribute does the work semantically; the asterisk is purely visual.
The most honest approach to required fields is to question whether you need them all. The best accessibility improvement to a required field is removing the requirement. Every field you can make optional, or eliminate entirely, reduces the cognitive and motor load on users. WCAG 3.3.7 (Redundant Entry) in WCAG 2.2 addresses this directly: don't ask users for information you've already collected.
If you do use the asterisk convention, the legend or explanatory note about it must appear before the first required field in the reading order, not after the form header or after the first input. It must also be visible to assistive technology. Don't hide it with aria-hidden or rely on the visual styling alone to communicate meaning.
Putting It Together
Accessible forms don't require a different design process. They require the same design process, done more completely. The label was already there; it just needed to be visible and persistent. The error was already planned; it just needed to be positioned and associated. The grouping was already implied visually; it just needed a semantic container.
Here's a practical checklist for every form you design:
- Every field has a visible, persistent label placed above the input, not inside it as a placeholder.
- Related fields are grouped with
<fieldset>and<legend>, especially radio buttons and checkboxes. - Error messages appear inline, below the field they describe, and are linked via
aria-describedby. Long forms also include an error summary that receives focus on submission failure. - Fields collecting personal information have documented
autocompletevalues in the design handoff. - Required fields use
aria-required="true". The asterisk (if used) is hidden from screen readers and the explanatory note precedes the first required field. - Validation timing is on blur or submission, never on every keystroke.
- All interactive states are designed: default, focus, error, disabled, and success.
Forms are one of the areas where accessibility and usability are most aligned. The patterns that help screen reader users, clear labels, explicit error messages, and logical grouping, are the same patterns that help every user. A well-labelled field is easier to understand for everyone. An error message that tells you how to fix the problem helps everyone. You're not designing for an edge case. You're designing for human memory and attention, which are limited regardless of disability.