Start a project

Article · Accessibility · Forms · WCAG · 14 min read

Accessible web forms checklist

An accessible web form is a conversation, not a picture of one. Someone should be able to complete it with a keyboard, a screen reader, a pointing device or a mix of all three, without guessing what a field is for or where they went wrong. The work is mostly ordinary HTML: a visible label for every control, the right autocomplete and type values, errors that are text as well as colour, and a focus order that matches the page.

On this page 10 sections
  1. A form is a conversation, not a visual layout
  2. Every control has a visible label
  3. Autocomplete, input types and names
  4. Errors, help text and the focus move
  5. Keyboard, focus order and the submit button
  6. Groups, required fields and disabled states
  7. Testing with a keyboard, a screen reader and a checker
  8. A copy-paste checklist
  9. Key takeaways
  10. Frequently asked questions

I use this checklist on the contact form on leej.uk and on the free tools that take input. The same habits transfer to a newsletter sign-up or a checkout. It is the web counterpart of the approach in Accessible iOS apps in SwiftUI: prefer the platform’s built-in controls, and test the path a person actually takes.

A form is a conversation, not a visual layout

A form fails when the visual layout is treated as the only interface. A column of boxes can look tidy and still be unusable if the labels live in placeholder text, the errors appear only as a red outline, or the submit button cannot be reached with the Tab key.

Think about the questions in order:

  1. What is this form for, in one sentence?
  2. Which fields are actually required to do that job?
  3. For each field, what should the browser or password manager already know?
  4. If something is wrong, how does the person find out, and how do they get back to the field?

The WCAG 2.2 criteria that come up most often are labels, instructions, error identification, error suggestion, name-role-value, and keyboard access. If you build for a public sector body in the UK, or for a service covered by the Equality Act, take advice for your own situation; a form that cannot be completed with a keyboard or a screen reader is already a poor form.

Keep the form short. Every extra field is another chance to get the label, the error and the focus wrong. Optional questions belong after the required ones, and should be marked optional in the label.

Every control has a visible label

Placeholder text is not a label. It disappears when the person starts typing, it is often too faint to meet contrast requirements, and a screen reader may not treat it as the accessible name. The same is true of a label that only exists in an aria-label you cannot see. Visible text, permanently next to the control, is the default that works.

The straightforward pattern is a <label> whose for attribute matches the control’s id:

<label for="email">Email address</label>
<input id="email" name="email" type="email"
       autocomplete="email" required>

Wrapping the control in the <label> also works, and is useful for checkboxes and radio buttons, where the clickable area should include the words:

<label>
  <input type="checkbox" name="updates" value="yes">
  Email me occasional updates
</label>

A few rules save rework:

The Form Accessibility & Autocomplete Auditor will catch missing labels, empty buttons and placeholder-only fields. It will not tell you whether the wording makes sense; read the form out loud as a conversation and fix anything you would not say to a person.

Autocomplete, input types and names

Browsers and password managers can fill a form only if you tell them what each field is. That is what autocomplete is for. The values are defined in the HTML specification, and they are worth using even on a short contact form.

A typical enquiry form should look something like this:

<label for="name">Full name</label>
<input id="name" name="name" type="text"
       autocomplete="name" required>

<label for="email">Email address</label>
<input id="email" name="email" type="email"
       autocomplete="email" required>

<label for="tel">Telephone (optional)</label>
<input id="tel" name="tel" type="tel"
       autocomplete="tel">

<label for="org">Organisation (optional)</label>
<input id="org" name="org" type="text"
       autocomplete="organization">

<label for="message">How can I help?</label>
<textarea id="message" name="message" required></textarea>

A few details matter:

If you later add a postal address, prefer the tokens street-address or address-line1, address-level2 (town or city), postal-code and country. UK postcodes belong in postal-code, not in a free-text “address” box if you can avoid it.

Errors, help text and the focus move

When a field is wrong, the person needs three things: to know that something failed, to know which field, and to know how to fix it. A red border on its own does none of those jobs for someone who cannot see it, and a toast that vanishes after two seconds helps no one.

A pattern that works:

  1. Validate on submit first, then optionally as the person leaves a field they have already filled. Don’t shout at an empty field the moment it is focused.
  2. Put a short summary at the top of the form after a failed submit, and move focus to it. List the errors as links that jump to the fields.
  3. Repeat the error next to the field, and point the field at that text with aria-describedby. Set aria-invalid="true" on the invalid control.
  4. Keep the help text as well as the error. If a password hint and an error share the field, include both ids in aria-describedby, in an order that still makes sense when read aloud.
<div id="form-errors" tabindex="-1" role="alert">
  <p>Please fix 2 problems in this form.</p>
  <ul>
    <li><a href="#email">Enter an email address in the shape name@example.com</a></li>
    <li><a href="#message">Enter a message</a></li>
  </ul>
</div>

<label for="email">Email address</label>
<p id="email-hint">I'll use this only to reply.</p>
<p id="email-error">Enter an email address in the shape name@example.com</p>
<input id="email" name="email" type="email" autocomplete="email"
       required aria-invalid="true"
       aria-describedby="email-hint email-error">

After a failed submit, call document.getElementById('form-errors').focus(). The tabindex="-1" makes that possible without putting the box in the tab order the rest of the time. role="alert" (or an aria-live region you already have) is how a screen reader hears that something happened; don’t also move focus and fire a second, contradictory announcement if you can help it.

Write the error as an instruction, not a scolding. “Enter an email address in the shape name@example.com” is better than “Invalid email.” If the message was too short, say what is needed. If a username is taken, say so, and offer a next step.

Client-side checks are a courtesy. They do not replace a server check, and they must not be the only way the error is shown. If JavaScript fails, the form should still submit and the server should still return a page with the errors in the HTML.

Keyboard, focus order and the submit button

If you cannot complete the form with the Tab key, a mouse and a screen reader only, it is not finished. The focus order should follow the visual order: labels and fields down the page, then the submit button, with no surprise jumps into the footer or out of a modal.

Check these by hand:

If the form sits in a dialog, the focus should move into the dialog when it opens, stay inside it while it is open, and return to the control that opened it when it closes. A contact page that is just a page does not need any of that machinery.

Groups, required fields and disabled states

Radio buttons that share a question belong in a <fieldset> with a <legend>. The legend is the question; each radio has its own label. Checkboxes that are independent answers do not need a fieldset, but a group of related choices (“How should I reply?”) usually does.

<fieldset>
  <legend>How should I reply?</legend>
  <label><input type="radio" name="reply" value="email" checked> Email</label>
  <label><input type="radio" name="reply" value="phone"> Telephone</label>
</fieldset>

Required fields should say they are required in the label, not only with a coloured asterisk or with the HTML required attribute. A short note at the top of the form, “All fields are required unless marked optional,” is enough if it is true. aria-required="true" can sit alongside the visible wording; it is not a substitute for it.

Avoid disabled on fields the person still needs to read or copy. A disabled input is often skipped by tab order and can be hard to perceive. If a field is not yet applicable, hide it, or leave it empty and explain when it will appear. readonly is the right choice when the value is part of the form but must not be edited, because it can still be reached and copied.

Don’t pre-tick marketing consent, and don’t bury it in the terms. If you need consent, ask for it in a labelled checkbox that is off by default, with a link to the policy. That is a legal question as well as an accessibility one; this checklist will not sign it off.

Custom selects, date ranges and file uploads need the same keyboard and screen-reader checks as the text fields. The ARIA Role & Attribute Validator will catch illegal role and attribute combinations; it cannot tell you whether the component actually works.

Testing with a keyboard, a screen reader and a checker

Automated checkers are a start, not a finish. I run them first, then I use the form the way a picky person would.

A practical sequence:

  1. Run a form auditor and an ARIA check on the live page, not only on a component in isolation. The Form Accessibility & Autocomplete Auditor and the ARIA Role & Attribute Validator live on this site for that pass.
  2. Unplug the mouse. Tab through every field, open any custom control, submit with errors, fix them, and submit successfully. Watch the focus. If you lose it, stop and fix that before anything else.
  3. Zoom the page to 200% and, if you can, use the browser’s larger-text settings. Labels and errors should still be visible. Fields should not overlap or overflow in a way that hides the control.
  4. Turn on a screen reader and complete the form once. On a Mac, VoiceOver is enough to catch missing names, duplicate names, and errors that are never announced. You do not need to be a daily VoiceOver user to notice “edit text” with no label.
  5. Fill the form with a password manager or the browser’s autofill. If the name lands in the message box, your autocomplete or name values are wrong.
  6. Submit with JavaScript disabled, or with a network error. The server response should still be an HTML page you can read and correct.

Write down the browsers and devices you actually tested. A form that works in one desktop browser can still fail in iOS Safari when the keyboard covers the error. The same discipline as a small iOS MVP applies: test the path, not the screenshot. Someone choosing an iOS developer can ask to see this pass, and App Store Accessibility Nutrition Labels should match it.

A copy-paste checklist

Use this as a review list before a form goes live, and again when you change a field. Tick only what you have actually checked.

If a form is long, split it into short steps with a clear title, and let people go back without losing what they typed.

Key takeaways

If you want a second pair of eyes on a form, tell me about it.

Frequently asked questions

How do I label a form field accessibly?

Use a visible <label> tied to the control with for and id, or by wrapping the control. The words in that label should be the accessible name. Don’t rely on placeholder text or a hidden aria-label that disagrees with what is on screen.

What autocomplete values should a contact form use?

For a simple enquiry: name (or given-name and family-name), email, tel if you ask for a phone number, and organization if you ask for a company. Leave a free-text message field without an autocomplete token. Use the values from the HTML specification rather than inventing your own.

How should form errors be announced to a screen reader?

After submit, put a short summary of the errors at the top, move focus to it, and give it role="alert" or an equivalent live region. Repeat each error next to its field and point the field at that text with aria-describedby. Set aria-invalid="true" on the control. Don’t rely on a toast that disappears.

Is placeholder text a good label?

No. Placeholder text disappears once the person types, is often low contrast, and is a poor accessible name. If you use a placeholder at all, use it as a short example, and keep a visible label outside the field.

How do I test a form with a keyboard only?

Put the mouse aside, press Tab until you reach the first field, and complete the form, including a failed submit and a successful one. You should see a focus ring on every control, be able to open and close any custom widget, and never get stuck. Then repeat the path with a screen reader and with the browser’s autofill.

Written by Lee Johnson. Published 10 November 2026.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project