Skip to content
  • FG-96
  • 14 min read
  • Last checked November 7, 2026

How to Make Website Forms Accessible: Labels, Error Messages, Autocomplete and CAPTCHA

Screen reader users cannot tell what a field wants, errors are invisible to them, or a CAPTCHA blocks them. Fix labels, errors, autocomplete, grouping and submit messages.

Run the health check

A screen reader user tabs into your contact form and hears “edit text, blank”. Then “edit text, blank” again, three more times. The visual labels are placeholders that vanished, or text sitting near the fields that is not connected to them. They guess, press Send, and nothing seems to happen: red borders appeared around two fields, but nothing was spoken. On the login page, a math puzzle or a distorted-letters CAPTCHA stands between them and their account.

Forms are where accessibility problems cost you directly: lost inquiries, abandoned bookings, failed checkouts and, increasingly, complaints. The good news is that HTML already does most of the work. Accessible forms come from a handful of correct patterns, plus choosing the right options in your form plugin. This guide covers those patterns and the plugin settings to look for. Spam protection in depth is covered in our separate guide on contact form spam, and forms that submit but never deliver email are covered in another.

What is happening

Every form field needs an accessible name, which the browser computes mainly from a label element connected to the field. Connection happens in one of two ways: the label for attribute matches the field id, or the field sits inside the label. Text that merely sits next to a field is not connected, and placeholder text is a weak fallback that disappears when typing starts. Errors work the same way: an error is accessible when its text is programmatically tied to the field and its appearance is announced.

Criterion Level What it means for forms
1.3.1 Info and Relationships A Labels, groups and required states shown visually are also in the code: label elements, fieldset and legend, the required attribute
3.3.2 Labels or Instructions A Fields that need input have labels or instructions, including format requirements
3.3.1 Error Identification A Detected errors identify the field in error and describe the problem in text
3.3.3 Error Suggestion AA When a fix is known, the error tells people how to correct it
1.3.5 Identify Input Purpose AA Fields that collect information about the user declare their purpose, in HTML with the autocomplete attribute
3.3.8 Accessible Authentication (Minimum) AA New in WCAG 2.2: login steps do not require a cognitive function test such as remembering or transcribing, unless an alternative or assistance is offered
4.1.3 Status Messages AA Messages such as “Message sent” or “3 errors” are announced by assistive technology without moving focus

WCAG 2.2 also added 3.3.7 Redundant Entry (Level A): in a multi-step process, do not make people type the same information twice, for example by offering “billing address same as shipping”.

Before you start

You need editor access to your form plugin or page builder form settings, a browser with developer tools, and a screen reader for testing (VoiceOver on Mac, free NVDA on Windows). Test on a staging copy or a duplicate form so test submissions do not reach your inbox or CRM.

How to run the commands in this guide. The commands are JavaScript you paste into the developer tools Console tab (F12, or Command+Option+I on a Mac) with the form page open. They read the page and do not change anything. The HTML examples show the target markup; if your form plugin generates the markup, use them to judge its output and to choose settings, not to edit plugin files.

Step 1: Audit the form the way a user meets it

Run axe DevTools on the form page and look for the rules label, select-name, autocomplete-valid, aria-valid-attr-value and color-contrast. Then list fields with no programmatic label:

Array.from(document.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]), select, textarea")).filter(function (el) {
  return !(el.labels && el.labels.length) && !el.getAttribute("aria-label") && !el.getAttribute("aria-labelledby");
});

No placeholders. It selects every visible kind of form field and keeps those without a connected label element, aria-label or aria-labelledby. Good output is []. Bad output lists the fields; hover each to highlight it. Note that a honeypot anti-spam field may appear here because it is deliberately hidden; check that it is also hidden from screen readers and skipped by Tab.

Then do the human test. With the keyboard only and a screen reader running, fill the form in, then submit it once empty and once with a deliberately wrong email address. Listen for: the label of each field, whether it is announced as required, any hint text, what happens on submit, and whether you can find and fix each error without seeing the screen.

Step 2: Give every field a real, visible label

The target markup for a field with a hint looks like this:

<label for="FIELD_ID">Email address <span aria-hidden="true">*</span></label>
<input id="FIELD_ID" name="email" type="email" autocomplete="email" required aria-describedby="FIELD_ID-hint">
<p id="FIELD_ID-hint">We only use this to reply to your message.</p>

FIELD_ID is a unique ID for the field, such as contact-email; form plugins generate these for you. The label for matches the input id, which connects them. The asterisk is hidden from screen readers with aria-hidden="true" because the required attribute already makes them announce “required”; explain the asterisk once at the top of the form (“Fields marked * are required”). aria-describedby points at the hint so it is read after the label. Good result in the Accessibility pane: Name “Email address”, Description “We only use this to reply to your message.”, and a required property.

Common label failures and their fixes:

  • Placeholder used as the label. The text disappears as soon as someone types, often has poor contrast, and is not reliably announced. Use a real label above the field; keep placeholders for optional examples only, or drop them.
  • “Hide label” turned on for a cleaner look. If a design truly needs no visible label (a single search field next to a Search button, for example), hide it visually with the theme screen-reader-text class rather than removing it. Most form plugins have a label visibility option; choose the “screen reader only” or “visually hidden” choice if offered, never “none”.
  • Instructions only at the top or in a tooltip. Put format rules (“DD/MM/YYYY”, “at least 12 characters”) in a hint tied with aria-describedby, visible before people type.

Step 3: Group related controls with fieldset and legend

A set of radio buttons labeled “Email”, “Text” and “Post” means nothing without the question. A group of three selects labeled “Day”, “Month” and “Year” needs to say it is the date of birth. Use fieldset and legend:

<fieldset>
  <legend>How should we contact you?</legend>
  <input type="radio" id="OPTION_ID_ONE" name="contact_method" value="email">
  <label for="OPTION_ID_ONE">Email</label>
  <input type="radio" id="OPTION_ID_TWO" name="contact_method" value="text">
  <label for="OPTION_ID_TWO">Text message</label>
</fieldset>

OPTION_ID_ONE and OPTION_ID_TWO are unique IDs for each option. The shared name makes the radios one group, so arrow keys move between them. Screen readers announce the legend when focus enters the group, for example “How should we contact you? grouping, Email, radio button, not checked, 1 of 2”. Good result: the legend is heard once on entry. Bad result: options announced with no question, which usually means the plugin wrapped them in a div with a styled heading instead of a fieldset. Most form plugins output fieldsets for radio, checkbox, name and address fields in their current markup; if yours does not, check for a newer markup option or a plugin update.

Step 4: Make errors findable, readable and fixable

Confirm the cause

Submit with errors. If the only change is a red border or a red asterisk, that fails 3.3.1 (no text) and WCAG 1.4.1 Use of Color. If text appears but the screen reader says nothing, the error is not announced. If text appears far from the field, or only at the top as “There was a problem with your submission”, people cannot tell which field to fix.

The pattern that works

  1. Write specific error text next to each field: “Enter an email address in the format name@example.com” rather than “Invalid input”. That covers 3.3.3 Error Suggestion.
  2. Tie the error to the field: add the error element ID to the field aria-describedby, and set aria-invalid="true" on the field. The screen reader then announces “invalid entry” and reads the error when the field gets focus.
  3. Show a summary at the top after submit: a heading such as “There are 2 problems with your form”, followed by a list of links, each pointing to the field in error (href="#FIELD_ID"). Move focus to the summary, so keyboard and screen reader users land on it.
  4. Do not clear what people typed, except in password fields.
  5. Validate on submit or when leaving a field, not on every keystroke, which floods screen reader users with messages while they are still typing.

The marked-up field in an error state looks like this:

<input id="FIELD_ID" type="email" autocomplete="email" required aria-invalid="true" aria-describedby="FIELD_ID-hint FIELD_ID-error">
<p id="FIELD_ID-error">Enter an email address in the format name@example.com</p>

FIELD_ID is the same field ID as before. aria-describedby can list several IDs separated by spaces; they are read in that order. Remove aria-invalid and the error ID once the field is corrected. Good result: tabbing to the field announces the label, “invalid entry”, the hint and the error. Bad result: the error text exists visually but is not read, which means the IDs do not match.

Native browser validation (the small bubbles that appear when required fields are empty) is announced by most screen readers but disappears quickly and cannot be styled. Many plugins turn it off with novalidate and use their own messages, which is fine as long as those messages follow the pattern above.

Step 5: Announce the submit result with a live region (4.1.3)

Most WordPress form plugins submit with AJAX: the page does not reload, and a message appears. Sighted users see “Thank you, your message was sent.” A screen reader user hears nothing, unless the message is inside a live region.

<div id="STATUS_ID" role="status"></div>

document.getElementById("STATUS_ID").textContent = "Thank you. Your message was sent and we will reply by email.";

STATUS_ID is any unique ID, such as form-status. The first line is HTML that must already be in the page when it loads; the second line is the JavaScript that fills it after a successful submit. role="status" creates a polite live region: the screen reader announces new text when it finishes what it is saying, without moving focus. For a failed submission, role="alert" announces immediately. The most common mistake is inserting the whole region along with its text at the same moment, which many screen readers ignore; the empty container must exist first. Good result: the message is spoken right after you press Send. To test a plugin, open the Elements panel before submitting and look for role="status", role="alert" or aria-live on the message container.

If your plugin replaces the whole form with the confirmation, an alternative is to move focus to the confirmation heading (give it tabindex="-1" so it can receive focus). Either way, someone who cannot see the page must learn that the submission worked.

Step 6: Add autocomplete attributes (1.3.5)

The autocomplete attribute tells browsers and assistive tools what a field is for. Browsers can then fill it from saved details, which helps people with motor and cognitive disabilities most, and some tools can show familiar icons beside fields. Use the standard tokens:

Field Token
Full name / first name / last name name / given-name / family-name
Email / phone email / tel
Company organization
Street address / city / state or region / ZIP code / country street-address or address-line1 / address-level2 / address-level1 / postal-code / country-name
Username / current password / new password username / current-password / new-password
Code from an authenticator app or text message one-time-code (an HTML token, not in the WCAG list, but it lets browsers offer the code)

1.3.5 applies to fields about the user. A referral form field asking for the name of a friend is not about the user, so it should not get name. Most current form plugins have an autocomplete setting on each field, often under advanced field options; WooCommerce checkout fields ship with autocomplete values already. Check the result with the Accessibility pane or by inspecting the input.

Step 7: Accessible Authentication and CAPTCHA alternatives (3.3.8)

WCAG 2.2 3.3.8 says a login step must not depend on a cognitive function test, such as remembering a password, transcribing characters or solving a puzzle, unless there is an alternative method or a mechanism that helps. In practice, a site meets it when:

  • Password managers work. Username and password fields have proper labels and the username and current-password autocomplete values, so browsers and password managers can fill them.
  • Paste is allowed in every field, including passwords and one-time codes. Scripts that block paste fail this criterion.
  • Puzzles have an alternative. A math question or distorted-text CAPTCHA on the login page is a cognitive function test. Recognizing objects in images is allowed at Level AA but still excludes some blind users, so offer another route.

Security plugins commonly add a math CAPTCHA to wp-login.php. Check your security plugin settings and switch to an option that does not ask people to solve anything, such as a non-interactive challenge, login rate limiting, or two-factor authentication with a code that can be pasted. Passkeys and email login links also avoid memory tests entirely.

On contact and booking forms, which are not authentication, the CAPTCHA rule in 1.1.1 still applies: a CAPTCHA needs a text alternative describing its purpose and alternative forms in different modalities. Non-interactive or invisible checks such as Cloudflare Turnstile in managed mode, score-based reCAPTCHA v3, honeypot fields and server-side filtering stop most automated spam without asking humans to solve anything. Our contact form spam guide explains how to layer them.

Step 8: Form plugin pitfalls and the settings to check

WordPress form plugins and builder form elements differ in markup quality, but the same pitfalls come up across them. Work through this list in your plugin settings:

  • Label visibility. Look for per-field label placement options; avoid “hidden” or “none” and use “screen reader only” where offered.
  • Markup version. Some plugins keep a legacy markup option for forms built in older versions. The newer markup is generally the more accessible; test after switching, since styling may change.
  • Required field indicator. Choose an option that uses text (“required”) or explains the asterisk, and confirm the field gets the required or aria-required="true" attribute.
  • Custom styled checkboxes, radios and selects. If the real input is hidden with display: none and replaced with a styled span, keyboard and screen reader users lose it. The input must stay in the page, visually hidden, not removed.
  • Multi-column layouts. Side-by-side fields should still follow the visual reading order in the source, so Tab moves left to right, top to bottom.
  • Conditional logic. Fields that appear based on an earlier answer should appear after the triggering field in the source, so people find them by tabbing forward.
  • Multi-page forms. Each page change should move focus to the new page heading and announce progress, such as “Step 2 of 3”.
  • Date pickers. Pop-up calendars are often mouse-only. Make sure people can type the date directly into the field.
  • Validation message position. Prefer inline messages plus a summary, not a summary alone.
  • AJAX confirmations. Check the message container for a live region role, as in Step 5.
  • Time limits. If a form or a security nonce expires, warn people before it happens and let them extend it, per WCAG 2.2.1 Timing Adjustable.

Advanced: testing forms in screen reader focus mode

Screen readers have two ways of working on web pages. In browse mode, letter keys jump around the page (F for the next form field in NVDA). When focus lands in a text field, NVDA switches to focus mode automatically, often with a short sound, so typed letters go into the field. VoiceOver on Mac handles this through its own interaction model, and Tab moves between form controls directly. This matters for testing: an error message that is not tied to a field with aria-describedby is easy to miss in focus mode, because the user is tabbing from field to field and never reads the text between them. Always test errors by tabbing, not by reading the page line by line.

A useful routine: in NVDA, press F repeatedly to visit each field in browse mode and confirm label, role, required state and description. Then press Tab through the same fields in focus mode and confirm the same information. Submit with errors and check that focus lands on the summary, the summary links move focus to the right fields, and each field reads its error. Finally, submit successfully and confirm you hear the confirmation. In VoiceOver, use the rotor (VO+U) and switch to the Form Controls list to review every field name at once.

When to contact your host

Sometimes a form appears inaccessible when the real problem is the server. If a submission returns a generic error or nothing at all for some visitors, check the Network tab: a 403 response on the form request usually means a web application firewall rule blocked it, which our 403 guide covers. Open a ticket with your host with the form page URL, the request URL, the exact time in UTC, the response status and any reference ID shown on the block page. Our diagnostic tools can help you check response headers first.

Not legal advice: this guide covers the technical side of accessible forms. If a form barrier was raised in a complaint or demand letter, involve a lawyer for the legal side.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close