On this page 10 sections
- A form is a conversation, not a visual layout
- Every control has a visible label
- Autocomplete, input types and names
- Errors, help text and the focus move
- Keyboard, focus order and the submit button
- Groups, required fields and disabled states
- Testing with a keyboard, a screen reader and a checker
- A copy-paste checklist
- Key takeaways
- 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:
- What is this form for, in one sentence?
- Which fields are actually required to do that job?
- For each field, what should the browser or password manager already know?
- 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 accessible name should match the visible label. If the on-screen text says “Email address” and the accessible name says “Your work email”, voice-control users who speak the words they can see will miss.
aria-labelis for cases with no visible text, not a second, secret name. - Icons need words. A magnifying glass, a trailing “X”, or a bare tick is not a name. Put the words in the button, or give the icon button a visible text alternative and an accessible name that matches.
- Don’t rely on colour or position alone. “The box on the right” or a red asterisk with no explanation will not survive a screen reader or a greyscale display.
- One label, one control. Don’t make a single visual label serve two inputs (for example “From” and “To” dates that only share the word “Dates”) without giving each input its own name.
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:
- Use the input type that matches the data.
type="email",type="tel",type="url"andtype="number"change the mobile keyboard and give you basic validation. Don’t usetype="number"for values that are not numbers in the mathematical sense, such as a postcode or a card number; those are text, with an appropriateautocompletevalue. nameshould be stable and meaningful. It is what your server receives, and it is also a hint for autofill on older browsers. Avoidname="field1".- Don’t turn autocomplete off unless the field is a one-time code or something a password manager must not store.
autocomplete="off"is ignored by some browsers anyway, and it makes life harder for people who rely on stored addresses. - Use the section prefixes when a form has two addresses, such as billing and delivery:
autocomplete="billing street-address"andautocomplete="shipping street-address". - A message or comments field usually has no autocomplete token. Leave it off, rather than inventing a value.
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:
- 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.
- 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.
- Repeat the error next to the field, and point the field at that text with
aria-describedby. Setaria-invalid="true"on the invalid control. - 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:
- Every control is reachable, including custom selects, date pickers, tag fields and “add another” buttons. If you replace a native
<select>or<input type="date">, you have taken on the keyboard behaviour as well. - Focus is always visible. The default outline is fine. If you restyle it, keep a contrast that is easy to see against both the light and dark backgrounds. The Contrast Checker is for text; for focus rings, look at the control in both themes rather than guessing.
- There are no keyboard traps. A date picker or autocomplete list must be escapable, and Tab must be able to leave it. The Focus Order & Keyboard Trap Analyser is a useful first pass; it does not replace a Tab through in the real browser.
- The submit control is a button.
<button type="submit">Send message</button>is the default. A<div>with a click handler will be missed by keyboard users unless you rebuild the button yourself, which you should not. - Don’t disable the submit button while the form is incomplete, if that is the only way you “validate”. A disabled button cannot explain what is missing. Leave it enabled, and show the errors on submit. A brief disabled state while the request is in flight is reasonable, as long as you also say that the form is sending, and you restore the button if the request fails.
- Tap targets are large enough on a phone. A 24-pixel checkbox with a tiny label will frustrate anyone on a train. The Tap Target & Focus Checker will flag tight hits; wrapping the control in the label is often the fix.
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:
- 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.
- 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.
- 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.
- 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.
- Fill the form with a password manager or the browser’s autofill. If the name lands in the message box, your
autocompleteornamevalues are wrong. - 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.
- The purpose of the form is obvious from the heading and the first sentence.
- Every field, checkbox, radio, select, file input and button has a visible label that matches its accessible name.
- Placeholder text, if used at all, is an example, not the label.
typeandautocompletevalues match the data, andnamevalues are stable.- Required fields are marked in the label; optional fields say they are optional.
- Radio groups and related checkboxes use a fieldset and legend.
- Help text is associated with the field through
aria-describedby. - Errors are identified in text, next to the field and in a summary, not only by colour.
- After a failed submit, focus moves to the error summary, and each error links to its field.
- Invalid fields have
aria-invalid="true"and an instruction that explains how to fix them. - The form can be completed with the keyboard alone, with a visible focus ring, and with no trap.
- The submit control is a real button, and it is not the only place an error is explained.
- The form still works when JavaScript fails; the server returns usable errors.
- Autofill, 200% zoom, a screen reader and a narrow phone viewport have all been tried.
- Third-party widgets have been through the same checks as the native fields.
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
- A form is a conversation: every control needs a visible, permanent label that matches the accessible name.
- Use native HTML first —
label, the righttypeandautocomplete,fieldsetandlegend, and a real submit button. - Errors must be text, announced, and linked to the field; a red outline is not enough.
- Keyboard order, a visible focus ring, and the absence of traps are part of the form, not polish.
- Automated checkers catch omissions; a Tab-only pass, a screen reader pass and an autofill pass catch the rest.
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.