Inclusive Form Design: 10 Patterns for Accessible Labels, Inputs, and Error Messages

Most forms fail people at exactly the worst moment: after they have already typed everything in. A missing label is annoying. A broken error message is a dead end. If a screen reader user cannot find out which field is wrong, or a person with dyscalculia cannot work out what “invalid format” means, they abandon the form and you lose the conversion.

This is a hands-on walkthrough of ten form patterns that work for screen reader users, keyboard-only users and people with cognitive disabilities. Each pattern includes the markup we actually ship, plus the common mistake developers make with it. Accessible form error messages are the backbone of the list, but they only work when labels, hints and validation timing are right too, so we start there. There is more on it in Technique: Form error communication.

The three jobs an accessible form has to do

Before the patterns, keep these three jobs in mind. Every decision below serves one of them:

  1. Prevent the error (clear labels, hints, forgiving input formats, autocomplete).
  2. Identify the error in text, programmatically tied to the field it belongs to.
  3. Explain the fix in plain language, and get the user back to the field fast.

WCAG 2.2 covers this across 3.3.1 Error Identification, 3.3.2 Labels or Instructions, 3.3.3 Error Suggestion and 1.4.1 Use of Color. That is the compliance floor. The patterns below are about making forms genuinely usable, which is a higher bar.

web form design

Pattern 1: Visible labels above the field, always

Put the label directly above the input, left aligned, and never remove it. Top-aligned labels give you room for long text, they translate well, and they stay visible when a mobile keyboard covers the lower half of the screen.

<div class="field">
  <label for="billing-postcode">Postcode</label>
  <input type="text" id="billing-postcode" name="postcode" autocomplete="postal-code">
</div>

The common mistake

Using the placeholder attribute as the label. The text disappears the moment someone types, it usually fails contrast requirements, and some voice control and translation tools ignore it. People with memory or attention difficulties are hit hardest: they lose the question while answering it. Placeholders are for examples (and even then, a visible hint is better).

Pattern 2: Hint text wired with aria-describedby before anything goes wrong

If a field has a rule, say the rule up front. Format requirements, password length, why you want a phone number. Announce it through aria-describedby so screen reader users hear it when they land on the field, not after they fail.

<label for="dob">Date of birth</label>
<p id="dob-hint" class="hint">For example, 27 3 1994</p>
<input type="text" id="dob" name="dob" inputmode="numeric"
       aria-describedby="dob-hint">

The common mistake

Overwriting aria-describedby when the error appears, which deletes the hint from the announcement. Keep both IDs, error first:

aria-describedby="dob-error dob-hint"
web form design

Pattern 3: Mark required fields in words, not just an asterisk

An asterisk alone relies on a legend the user has to remember. Spell it out in the label, and keep required on the control so the browser and assistive tech agree. For the wider picture, see ARIA: aria-errormessage attribute.

<label for="email">Email address <span class="req">(required)</span></label>
<input type="email" id="email" name="email" required autocomplete="email">

If nearly every field is required, flip the logic and mark the optional ones instead. It is less visual noise.

The common mistake

Adding aria-required="true" next to a native required attribute (redundant), or using aria-required on a div that is not a real form control. Use native HTML controls first, ARIA only to fill gaps.

Pattern 4: Group related controls with fieldset and legend

Radio buttons, checkbox sets and multi-part inputs such as a split date need a group name. Without it, a screen reader user hears “Day, edit” with no idea what the group is asking. For the wider picture, see Accessible Form Validation: Best Practices.

<fieldset>
  <legend>How should we contact you?</legend>
  <input type="radio" id="c-email" name="contact" value="email">
  <label for="c-email">Email</label>
  <input type="radio" id="c-phone" name="contact" value="phone">
  <label for="c-phone">Phone</label>
</fieldset>

For a group-level error, describe the whole group from the first control or use role="group" with aria-describedby on the container:

<fieldset role="group" aria-describedby="contact-error">

The common mistake

Wrapping every single input in its own fieldset. Groups of one are noise. Use them only where controls genuinely belong together.

Pattern 5: Validate on blur, never on every keystroke

Inline validation timing is where most “accessible” forms fall apart. Validating as characters are typed means the user is told they are wrong while they are halfway through being right. Screen readers can interrupt themselves repeatedly, and people with anxiety or cognitive fatigue get punished for typing slowly.

A timing model that holds up in testing:

Moment What to do
While typing (first attempt) Say nothing. Show live helpers only if they are positive, such as a password strength meter.
On blur Validate the field the user just left and show an inline error if needed.
While typing (after an error) Clear the error as soon as the input becomes valid. Do not re-error mid-typing.
On submit Validate everything, render the error summary, move focus to it.

The common mistake

Binding validation to the input event with no debounce, then moving focus to the offending field automatically. Stealing focus mid-typing is one of the fastest ways to make a form unusable with a keyboard or a screen reader.

web form design

Pattern 6: The inline error message, correctly wired

This is the core pattern. An inline error needs four things: visible text, an icon or shape so it does not rely on colour alone, aria-invalid="true" on the control, and aria-describedby pointing at the message.

<div class="field field--error">
  <label for="email">Email address (required)</label>
  <p id="email-hint" class="hint">We use this to send your receipt</p>
  <p id="email-error" class="error">
    <span aria-hidden="true">⚠</span>
    Error: Enter an email address in the format [email protected]
  </p>
  <input type="email" id="email" name="email" required
         aria-invalid="true"
         aria-describedby="email-error email-hint">
</div>

Place the message between the label and the input, or immediately under the input. Above the input has a practical advantage: on mobile it is never hidden by the on-screen keyboard.

The common mistake

Leaving aria-invalid="true" on a field permanently, or setting it on page load for empty required fields. Every field is then announced as invalid before the user has done anything wrong. Set it when validation fails, remove it (or set it to false) when it passes.

Pattern 7: A focusable error summary after submit

After a failed submit, render a summary at the top of the form, move focus to it, and link each item to the field that needs fixing. This gives keyboard users a one-tab route to every problem and gives everyone a count of what is left to do.

<div class="error-summary" role="alert" tabindex="-1" id="error-summary">
  <h2>There are 2 problems with this form</h2>
  <ul>
    <li><a href="#first-name">Enter your first name</a></li>
    <li><a href="#dob">Date of birth must include a year</a></li>
  </ul>
</div>

Then in JavaScript, after inserting the summary:

document.getElementById('error-summary').focus();
document.title = '2 errors: Checkout - Your Company';

Updating the page title helps people who navigate between tabs and users of screen readers that announce the title on change.

The common mistake

Building the summary but forgetting tabindex="-1", so focus() silently does nothing and the user is left at the bottom of the page wondering why the submit button did not work. Second most common: the links point to a wrapper div instead of the input’s own id, so focus never lands in the field.

web form design

Pattern 8: Write the message like a human, not like a validator

Wording is an accessibility feature. People with cognitive disabilities, non-native speakers and anyone in a hurry all need the same thing: name the field, say what is wrong, say what to do. Keep it under about 15 words and avoid blame.

Avoid Use instead Why
Invalid input Enter an email address in the format [email protected] Gives the fix, not just the verdict
This field is required Enter your postcode Works out of context, in a summary list
Password does not meet policy Password must be at least 12 characters States the actual rule
You entered the date wrong Date of birth must include a year, for example 1994 No blame, includes an example
Error 422: constraint violation We could not save your details. Try again in a moment. Server language is not user language

Prefix inline messages with the word “Error:” so the state is clear even when the message is read on its own. Keep the same wording in the inline message and the summary item so users do not think they are two different problems.

The common mistake

Relying on the browser’s default validation bubble. Those bubbles are styled by the browser, wording varies between browsers and languages, they vanish on their own, and they are not reliably exposed to all assistive tech. Call e.preventDefault() on submit and render your own messages, or at minimum use setCustomValidity() plus your own visible text.

Pattern 9: Use live regions sparingly, and only for changes the user did not trigger

For asynchronous checks such as “this username is taken” or “promo code applied”, the user gets no announcement unless you provide one. A polite live region that is already in the DOM before the message arrives is the reliable approach.

<!-- present on page load, empty -->
<div id="async-status" role="status" aria-live="polite"></div>
document.getElementById('async-status').textContent =
  'Username taken. Try adding a number.';

Rules of thumb:

  • role="status" / aria-live="polite" for almost everything.
  • role="alert" only for the post-submit summary or something genuinely urgent.
  • One live region per purpose. Several competing regions produce announcement chaos.
  • Do not put field-specific errors only in a live region. They still need to be readable from the field itself.

The common mistake

Injecting an element that already contains text and then adding aria-live, or toggling the region with display:none. Both frequently result in nothing being announced. Insert the empty region first, update its text content later.

Pattern 10: Design out the error before it happens

The most accessible error message is the one nobody ever sees. Cheap wins that measurably reduce error rates:

  • Autofill support: correct autocomplete tokens on name, email, address, phone and payment fields. This also satisfies WCAG 1.3.5 Identify Input Purpose.
  • Forgiving formats: strip spaces, dashes and brackets from card numbers and phone numbers server side instead of rejecting them.
  • No maxlength surprises: do not silently truncate. Tell the limit up front.
  • Do not ask twice: WCAG 2.2’s 3.3.7 Redundant Entry means avoiding repeated “confirm your email” fields where you can. Let people paste instead.
  • Let password managers work: 3.3.8 Accessible Authentication rules out blocking paste in password and one-time-code fields.
  • Preserve input on failure: never wipe the form when validation fails. Losing 20 fields over one typo is a hostile experience.
  • Confirm success: an accessible success message closes the loop for users who cannot see a visual state change.

The common mistake

Aggressive regex. Rejecting valid email addresses with unusual domains, valid UK postcodes, apostrophes in surnames or non-Latin characters in names creates errors that literally cannot be fixed. Validate loosely, verify properly.

web form design

Quick audit checklist

Run this on any form before shipping. It takes about ten minutes:

  1. Tab through the whole form with no mouse. Can you reach and operate everything, in a sensible order, with a visible focus indicator?
  2. Submit the form empty. Does focus move somewhere useful, and is an error count announced?
  3. Tab from the summary to a field. Do you hear the label, the hint and the error?
  4. Turn the display to greyscale. Are error states still obvious without colour?
  5. Zoom to 400% or set a 320px viewport. Do messages stay next to their fields?
  6. Check that error text meets contrast requirements. Light red on white usually does not.
  7. Fix one field. Does its error clear, and does the summary update its count?
  8. Test with a real screen reader: NVDA or JAWS with Chrome or Firefox, VoiceOver with Safari, TalkBack on Android.

Reference: the ARIA attributes that matter on a form field

Attribute Purpose When to set it
for / id Binds label to control Always
aria-describedby Links hint and error text to the control Whenever a hint or error exists
aria-invalid Exposes the error state Only while the field is actually invalid
role="alert" Interrupts to announce urgent content Post-submit error summary
role="status" Polite announcement of changes Async validation, saved states
autocomplete Identifies input purpose, enables autofill Any field about the user
tabindex="-1" Makes a container programmatically focusable Error summary containers

FAQ

Should error messages go above or below the input?

Either works if the message is wired with aria-describedby. Above the input (between label and field) is the safer default because a mobile keyboard or an autocomplete dropdown can cover text placed below. Be consistent across the whole product.

Do I need role=”alert” on every inline error message?

No. An inline error on a field the user just left will be read when they return to that field, thanks to aria-describedby. Sprinkling role="alert" on every field error creates a stream of interruptions. Reserve it for the post-submit summary.

Is aria-invalid enough on its own?

No. aria-invalid="true" only communicates “something is wrong here”. WCAG 3.3.1 requires the error to be described in text, and 3.3.3 requires a suggested fix where one is known. Always pair the attribute with a visible message.

Can I rely on native HTML5 validation?

Use native attributes such as required, type="email" and inputmode as a base layer, because they improve keyboards on mobile and give you free server-independent checks. Do not rely on the browser’s error bubbles as your only feedback: wording, styling and assistive tech support vary too much. Render your own messages.

How do I handle errors on multi-step forms?

Validate each step before advancing, show a step-level summary with focus moved to it, and never let a later step silently invalidate an earlier one. If a problem originates in an earlier step, link back to the exact field and preserve everything the user already entered.

What about colour for error states?

Colour can support the error state but must never be the only signal. Add an icon or a thicker border on the left of the field, keep the message in text, and make sure error text and borders meet the contrast requirements of WCAG 1.4.3 and 1.4.11.

The short version

Visible labels above the field. Hints before errors, not after. Validate on blur, summarise on submit. Tie every message to its field with aria-describedby, flag the state with aria-invalid, and write the text as if you were helping someone in person. Do that consistently and your forms will be accessible, faster to complete and better converting, in that order.