1. Interaction
  2. Forms
  3. Accessibility and Usability
  4. Form Accessibility and Usability

Interaction · Forms 04

Form Accessibility and Usability

People give up on forms for two reasons: too much effort and too many errors. Most of the cure is already in HTML: constraints that catch mistakes early and say how to fix them, the right keyboard on a phone, autofill that types for the person, and markup every reader can follow. All of it is a service to the person filling in the form. None of it protects the server.

Organizing Forms gave every control a label, every group a legend and every message a place in an <output>, held together by <form-field>. This page uses that pattern and asks what happens when a real person fills it in: on a phone, with a screen reader, in a hurry, after a typo. It works through a sign-up form for the Trail Notes site and covers four things in turn: catching mistakes, talking about them, saving effort, and checking that everyone can get through.

1. Effort and errors

Every form asks a favor. The person wants something on the other side of it, an account, a ticket, an answer, and the form stands in the way. They will put up with some effort, but each extra tap, each retyped address, each confusing error spends a little of their patience, and when it runs out they leave. Two things spend it fastest:

  • Effort. Typing on a phone keyboard that hides the @ sign. Entering a postal address that the browser already knows. Finding a tiny checkbox with a thumb. Typing a password by hand because the site blocked paste.
  • Errors. Being told something is wrong before having a chance to finish. Being told something is wrong without being told what. Seeing a red border with no words, or words with no indication of which field they belong to. Worst of all, submitting a long form and getting it back empty.

The tools on this page reduce one or the other. Constraints such as required and type="email" let the browser catch a mistake while the person is still on the page, instead of after a round trip. Good timing and clear messages turn a mistake into a quick fix. The right type, inputmode and autocomplete mean less typing and fewer typos in the first place. And the accessibility checks at the end make sure all of that reaches people who use a keyboard, a screen reader, voice control or a zoomed screen.

Keep one idea in view throughout. Everything here runs in the person's browser, under the person's control, and can be switched off in a second. That is fine, because none of it is meant to stop anyone; it is meant to help. Form Security explains why the server must check every value again, as Patterns: a form is a network client already put it: client-side validation is user experience, not protection.

Usability and accessibility overlap almost completely in forms. A message in text, not only in color, helps a person who cannot see red and a person in bright sunlight. A large target helps a person with a tremor and a person on a bus. The course's quality pages on usability and accessibility describe each quality across the whole client; this page is where they meet forms.

2. Constraints that help

HTML lets a control say what a good value looks like. The browser checks the value against those rules, a process the standard calls constraint validation, before the form is submitted, and it exposes the result to CSS and to scripts. A control that fails a rule is said to be suffering from that problem, and each problem has a name in the control's validity object, which section 6 takes up.

AttributeWhat it asks forFlag when it fails
requiredSome value. For a checkbox, checked; for a radio group, one chosen; for a select, an option whose value is not empty.valueMissing
type="email", type="url"A value in that syntax. An email needs an @ with something on each side; a URL needs a scheme.typeMismatch
patternA value the regular expression matches in full.patternMismatch
min, maxA number, date or time within the range.rangeUnderflow, rangeOverflow
stepA value that is a whole number of steps from min (or from zero).stepMismatch
minlength, maxlengthA length, in characters, the person has typed.tooShort, tooLong
(any typed control)Input the browser could turn into a value at all.badInput

required, and saying so

required is the simplest constraint and the one that does the most. The browser will not submit the form while the field is empty, assistive technology announces the field as "required," and CSS can select it with :required. It still has to be said in words on the screen, as Organizing Forms describes: mark the optional fields when most are required, or the required ones when most are optional.

One trap: a value of only spaces is a value. required on a text field accepts " ". The server will trim and check it anyway; a hint that says what is needed helps the person more than a cleverer rule.

type: a constraint and a keyboard

Choosing the right type does two jobs. It sets a rule, so type="email" rejects sam.example.com, and it chooses the keyboard on a phone, which section 7 covers. The rules are deliberately loose. The email rule accepts a@b, because addresses on a local network are legal; it cannot know whether a mailbox exists. A type="tel" field has no rule at all, because phone numbers around the world have too many formats; it only brings up the keypad. Form Fields describes each type and what it sends.

pattern: anchored, and explained

pattern holds a regular expression that the whole value must match. The browser wraps it in an implied ^(?: at the start and )$ at the end, so pattern="[0-9]" means exactly one digit, not "contains a digit." A five-digit postal code with an optional four-digit extension looks like this:

<form-field> <label for="zip">ZIP code</label><br> <input id="zip" name="zip" type="text" inputmode="numeric" autocomplete="postal-code" required pattern="[0-9]{5}(-[0-9]{4})?" title="5 digits, or 5 digits, a hyphen and 4" aria-describedby="zip-hint zip-error"><br> <output id="zip-hint" class="hint" for="zip">5 digits, such as 92093.</output><br> <output id="zip-error" class="error" for="zip" aria-live="polite"></output><br> </form-field>

Three details matter. First, a pattern is invisible: nobody can see it, so the format must be said in the hint, before anyone types, never discovered by trial and error. Second, the title attribute is optional extra: some browsers add its text to their own error message, so word it as a description ("5 digits"), not as an error, because it also appears as a tooltip on hover. It is not a substitute for the hint. Third, since 2023 browsers compile pattern with the regular expression v flag, which is stricter about character classes: inside square brackets, a literal -, (, ), [, {, }, / or | must be escaped with a backslash. An older pattern such as [a-z0-9_-] is now an invalid expression, and browsers ignore a pattern they cannot compile, so the field silently accepts anything. Write [a-z0-9_\-].

Patterns are good at fixed formats: codes, identifiers, a username's allowed characters. They are bad at people's names, addresses and phone numbers, which vary more than any expression will allow. A pattern that rejects a real name is not a constraint; it is a locked door. Forms for People takes that up.

min, max and step

On number, range, date and time controls, min and max set the allowed range and step sets the granularity. The default step for a number is 1, which surprises everyone who builds a price field: 12.50 fails with stepMismatch until the field says step="0.01", or step="any" to allow any decimal. A date field with min set to today, written by the server into the page, keeps a booking out of the past and greys out earlier days in the picker.

<label for="party">Hikers in your group</label> <input id="party" name="party" type="number" min="1" max="12" value="1"> <label for="fee">Donation (USD)</label> <input id="fee" name="fee" type="number" min="0" step="0.01">

minlength and maxlength

maxlength stops the person typing past the limit; most browsers simply refuse further keystrokes, so tooLong rarely appears. That is friendly for a short code and unfriendly for prose, because a pasted paragraph is silently cut off. For a text area, a visible count ("412 of 500 characters") in the hint is kinder than a hard stop. minlength works the other way: it lets the person type anything but flags a value that is too short. Both count only what the person typed; a value set by a script is not checked against them. An empty value is never "too short," so pair minlength with required if the field must be filled in.

badInput: what the browser could not read

A control with a structured value, a number or a date, can receive text the browser cannot convert. Typing 12..5 into a number field, where the browser allows it, leaves the field suffering from badInput, and its value property reports an empty string, because there is no number to report. A script that reads value and sees "" cannot tell "nothing typed" from "something unreadable typed"; validity.badInput can.

3. Showing state: :user-invalid, not :invalid

CSS can see the result of constraint validation through pseudo-classes. :valid and :invalid match whenever the value currently passes or fails. That sounds right and is not, because an empty required field is invalid from the moment the page loads. A style sheet that paints :invalid red greets every visitor with a form full of errors they have not made.

/* Every empty required field is red on page load */ input:invalid { border-color: red; }

:user-invalid and :user-valid fix this. They match only after the person has interacted with the control: changed its value and moved away, or tried to submit the form. Before that, an untouched field matches neither, so it looks neutral. Both are Baseline widely available; every current browser has supported them since November 2023.

When :invalid and :user-invalid match a required email fieldFour moments in the life of a required email field: the page loads with it empty; the person is typing sam@; the person leaves the field with sam@; the person fixes it to sam@example.com. The :invalid row matches in the first three moments, including on page load before anything was typed. The :user-invalid row matches only in the third moment, after the person has left the field with a bad value, and stops matching when it is fixed.1 page loads(empty)2 typingsam@3 leaves fieldsam@4 fixes itsam@example.com:invalidmatchesmatchesmatchesno:user-invalidnonomatchesno:invalid scolds at moments 1 and 2:user-invalid waits until the person has had a go
Both pseudo-classes see the same validity. :user-invalid adds one condition: the person has interacted with the field, or tried to submit.

The house CSS from Organizing Forms already uses it. Here it is with the message styles, written so the state never depends on color alone:

@layer components { :root { --error: light-dark(#b3261e, #ff8a80); --ok: light-dark(#1b6e3a, #7ad69a); } form-field { & output.error { color: var(--error); font-weight: 600; } & output.error:empty { display: none; } &:has(:user-invalid) :is(input, select, textarea) { border-color: var(--error); box-shadow: 0 0 0 1px var(--error); } /* Without a script, the hint becomes the message */ &:has(:user-invalid) output.hint { color: var(--error); } &:has(:user-valid) :is(input, select, textarea) { border-color: var(--ok); } } }

The thicker border is the shape cue; the words in the output are the real message. With no script at all, the browser still blocks the submission and shows its own message for the first problem, and the style sheet turns the hint, which already says what is needed, into the error color once the person has left a field. That is a reasonable floor. A script can then write a specific message into the error output, which section 5 covers.

Use :user-valid sparingly. A green check on every field the person has filled in is noise; on a field that is easy to get wrong, such as a new password with rules, it is a useful "this one is done." Other state pseudo-classes, :required, :optional, :in-range, :out-of-range and :placeholder-shown, are there when a design needs them; the same rule applies to each: style what the person has done, not what they have not done yet.

4. When to check

A form can tell the person about a problem at three moments: while they type, when they leave the field, or when they submit. Each has a cost.

  • As typed. Checking on every keystroke reports problems that are not problems yet. Someone typing an email address is "invalid" until they reach the domain; telling them so after the first letter is like correcting someone mid-sentence. For a screen reader user, an error announced on every keystroke is a stream of interruptions.
  • On leaving the field. When focus moves on, the person has said "this one is done." That is the right moment to check, while the field is still fresh in their mind and before they have filled in five more.
  • On submit. Waiting until the end means the person learns about every problem at once, possibly after the page has reloaded, and has to find each field again. The browser's own check happens here, and it is the last chance, not the first.

The pattern that works best combines them: check a field when the person leaves it; once a field is showing an error, check again as they type, so the message disappears the moment the value is right; and check everything on submit, for the fields nobody visited. Designers sometimes put it as "reward early, punish late": report success quickly, report failure only after the person has had a fair chance.

Three moments to report an errorA timeline for one field: focus, typing, leaves, returns and fixes, submits. Checking as typed shows an error while the person is still typing, too early. Checking on leaving shows the error when the person leaves, then clears it as soon as they fix it. Checking only on submit stays silent until the end and then reports every error at once.focustypingleavesreturns, fixessubmitsas typederrorcleartoo earlyon leavingerrorclearstart hereon submitalltoo late aloneonce an error shows, recheck as the person types so it clears the moment the value is right
Check on leaving first, clear as soon as the value is fixed, and check everything on submit for the fields nobody reached.

Notice that :user-invalid already behaves this way: it waits until the person leaves the field, then updates live as they type. The CSS gets the timing right for free; a script that writes messages should follow the same rhythm. A few checks are worth running as the person types from the start, because they help rather than scold: a character count that goes up, a password rule that ticks off as it is met. The difference is tone. Progress is welcome at any time; complaints wait.

5. Messages that say what to do

An error message has one job: get the person to a valid value as fast as possible. That means saying which field, what is wrong, and how to fix it, in words, next to the field.

Instead ofSayWhy
Invalid inputEnter an email address, like sam@example.comNames the field and shows a good value.
Error 422: field_zipEnter 5 digits for the ZIP codePlain words, not the program's internals.
Password does not meet requirementsUse at least 12 characters; you have 9Says exactly which rule and how far off.
You must agreeTo create an account, agree to the trail rulesSays why the box matters.
Oops, something went wrongWe could not save your report. Your answers are still here; try again in a minute.Says what happened to their work and what to do next.

Write messages in the same calm voice as the rest of the form. Avoid blame ("you entered an invalid…"), jokes, and exclamation marks; a person who has just been told no does not want to be cheered up. If the form knows a likely fix, offer it: "Did you mean sam@gmail.com?" WCAG calls this error suggestion and requires it when the fix is known and offering it would not compromise security.

In text, in its place

Color alone cannot carry an error. Color vision deficiency is common, especially among men, a screen reader does not read colors, and bright sunlight washes them out. The message goes in words, in the field's output.error, which Organizing Forms set up: tied to the control with aria-describedby, so the message is read as the control's description when the person returns to it, and a polite live region, so it is announced when it appears. The red border and an icon are reinforcements. Color for people applies the same rule to a whole page.

Keep the hint as well as the error. The hint says what is expected; the error says what went wrong. When both are present the person hears "Email, edit text, required, invalid. We send one confirmation, nothing else. Enter an email address, like sam@example.com." That is long, so keep each part short.

Focus to the first problem

When a submission fails, the person needs to know that it failed and where to start. With no script, the browser does this: it cancels the submission, moves focus to the first invalid control in document order, and shows its own message beside it. A scripted form must do at least as much. If it shows a summary of all errors at the top, a list of links to each field, it moves focus to that summary, so a screen reader reads it and a keyboard user can follow the links; otherwise it moves focus to the first invalid field. It never leaves focus on the submit button with errors scattered somewhere above it, out of sight.

Never clear what was typed

A form that comes back empty after one mistake punishes the person for every field they got right. Keep every value. When the server rejects a submission, it sends the page back with the values filled in and the messages in place, so only the bad field needs attention. The one exception is a password, which the server should not echo back into the page; say plainly that it needs to be typed again. A scripted form has it easier: it simply does not touch the fields it is not complaining about.

What the small script does

HTML and CSS take this a long way. The browser checks constraints on submit and focuses the first problem; :user-invalid marks a field once the person has left it. What they cannot do is put specific words into the error output at the right moment, so that the live region announces them. That is a few lines of script: listen for focus leaving a field the person has typed in, and for the form's invalid events, read the control's validity, write a message into its output.error, and clear it once the value is fixed. The demo on Organizing Forms does exactly this, and so does the one below. The events involved, focusout, input, invalid and submit, belong to Events: forms.

const form = document.querySelector('form'); const errorFor = (control) => control.closest('form-field')?.querySelector('output.error'); const show = (control) => { const out = errorFor(control); if (out) out.textContent = control.validity.valid ? '' : control.validationMessage; }; const touched = new WeakSet(); // fields the person has typed in form.addEventListener('focusout', (e) => { if (e.target.willValidate && touched.has(e.target)) show(e.target); }); form.addEventListener('input', (e) => { touched.add(e.target); if (errorFor(e.target)?.textContent) show(e.target); }); // invalid does not bubble, so listen in the capture phase form.addEventListener('invalid', (e) => show(e.target), true);

The script adds words; it removes nothing. With the script missing, the form still checks, still blocks, still focuses the first problem and still marks fields with CSS. That is progressive enhancement in a dozen lines.

6. The Constraint Validation API, in outline

Every form control that takes part in validation, and the form itself, exposes the same small set of properties and methods. This page only outlines them; Events: forms uses them in working code.

MemberOnWhat it does
validitycontrolA ValidityState object: one true-or-false flag for each problem in section 2, plus customError, plus valid, which is true when every other flag is false.
validationMessagecontrolThe browser's message for the current problem, in the browser's language, or an empty string.
willValidatecontrolFalse for controls that are never checked: disabled ones, readonly inputs, hidden inputs, buttons that are not submit buttons, outputs.
checkValidity()control or formReturns true or false and fires an invalid event at each failing control. Shows nothing.
reportValidity()control or formDoes the same, then shows the browser's message for the first problem and focuses it, as a submit would.
setCustomValidity(message)controlMarks the control invalid with your message (customError), until called again with an empty string.
requestSubmit(submitter)formSubmits the form as if a button were pressed: validation, the submit event, then the request.

Rules HTML cannot say

Some rules involve more than one field or knowledge the attributes do not have: "repeat the email," "the end date is after the start date." setCustomValidity() plugs such a rule into the same machinery. Set a message and the control becomes invalid, matches :user-invalid, blocks submission and shows your text; set an empty string and it is valid again. The common mistake is forgetting the second half: a custom error stays until something clears it, so the check must run again whenever either field changes.

const email = form.elements.email; const again = form.elements['email-again']; const match = () => again.setCustomValidity( again.value && again.value !== email.value ? 'Type the same email address in both boxes.' : '' ); email.addEventListener('input', match); again.addEventListener('input', match);

requestSubmit, not submit

A form has two methods that sound alike. form.requestSubmit() behaves like a person pressing a submit button: it runs constraint validation, fires the submit event so listeners can act, and then sends the request. Pass it a button, form.requestSubmit(saveButton), and that button is the submitter, so its name and value are sent and its formaction or formnovalidate apply. The older form.submit() skips all of that: no validation, no submit event, no submitter. Old tutorials use submit(); a script that wants the form to behave like a form uses requestSubmit(), which is Baseline widely available.

novalidate and formnovalidate

Sometimes the browser's check is the wrong one for the moment. Two attributes turn it off.

  • formnovalidate on a submit button skips validation for submissions made with that button only. The classic use is "Save draft": a half-finished form should be saveable, and a required field that is still empty is not an error in a draft. The button's name and value tell the server which button was pressed, as The form and its request showed.
  • novalidate on the form turns off the browser's checking on submit entirely. A script that wants to show its own error summary instead of the browser's bubbles sets it, and then calls checkValidity() itself in its submit handler. The constraints still work: validity, :user-invalid and checkValidity() all still see them. Only the automatic blocking is gone, so a script that sets novalidate has taken on the job of blocking, and should add the attribute from script, so that the form keeps the browser's checking when the script does not run.
<p> <button type="submit" name="action" value="join">Create account</button> <button type="submit" name="action" value="draft" formnovalidate>Save draft</button> </p>

7. The right keyboard

On a phone, the keyboard is part of the form. A person typing an email address on the default keyboard has to switch layers to find @; a person typing a ZIP code on a full keyboard taps between tiny letter keys to reach the digits. Each switch is a moment of effort and a chance to mistype. HTML lets the form choose the keyboard.

type chooses the keyboard

The control's type is the first choice, because it brings the keyboard and the right rules together. Exact layouts vary by phone, by operating system and by the person's language, so the table describes what to expect, not what is guaranteed.

typeWhat a phone usually brings up
textThe full keyboard, with capitalization and suggestions on.
email@ and . on the first layer; no automatic capital letter.
url/ and . keys, often a .com key; no automatic capital letter.
telA phone keypad: large digits, with *, # and +.
numberDigits, usually with a decimal point and a minus sign; varies the most between phones.
searchThe full keyboard with the Enter key labeled for searching.
passwordThe full keyboard with suggestions and learning turned off; a password manager may offer to fill or create a password.
date, timeA picker instead of a keyboard.

inputmode when the type would be wrong

Sometimes the right keyboard and the right type disagree. A card number, a ZIP code, a one-time code and a student ID are all made of digits, but they are not numbers: nobody adds them up, a leading zero matters, and type="number" would drop it, add spinner arrows and change the value when the person scrolls over the field. Form Fields explains why. The answer is type="text" for the rules and inputmode for the keyboard. inputmode changes nothing but the keyboard; it adds no constraint, so pair it with a pattern if the value must be digits.

inputmodeKeyboardUse for
numericDigits 0 to 9ZIP codes, card numbers, PINs, one-time codes
decimalDigits and the locale's decimal separatorPrices and measurements in a text field
tel, email, url, searchAs the matching typeWhen the type cannot be used, which is rare
textThe full keyboard (the default)Resetting a value inherited from elsewhere
noneNo virtual keyboardOnly when the page supplies its own input, such as an on-screen keypad
<label for="code">Verification code</label> <input id="code" name="code" type="text" inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code" maxlength="6">

enterkeyhint: label the Enter key

The Enter key on a phone keyboard can say what pressing it will do. enterkeyhint takes one of seven values, enter, done, go, next, previous, search and send, and the keyboard shows a matching word or symbol. It is only a label: enterkeyhint="next" does not move focus to the next field by itself, and pressing Enter in a single-line field can still submit the form, as it always has. Use it to tell the truth about what Enter does, send on a message box whose Enter submits, search on a search field, not to promise behavior the page does not have. Both inputmode and enterkeyhint are Baseline widely available.

8. Autofill: let the browser type

The cheapest field is the one the person does not have to type. Browsers and password managers remember names, addresses, phone numbers, card details and passwords, and can fill a whole form with one tap. They do it well when the form says what each field is, and badly when they have to guess from labels and names. The autocomplete attribute says what each field is.

Tokens

Despite its name, autocomplete is not on-or-off. Its value is a list of tokens drawn from a fixed vocabulary in the HTML standard, and the token that names the field matters most. The common ones:

KindTokens
Namesname, or given-name and family-name if the form insists on splitting; nickname; honorific-prefix
Contactemail, tel, url
Addressstreet-address (one text area), or address-line1 and address-line2; address-level2 (city), address-level1 (state or province), postal-code, country or country-name
Accountusername, current-password, new-password, one-time-code
Paymentcc-name, cc-number, cc-exp, cc-csc
Otherbday, organization, language, sex

Sections, shipping and billing

Some forms ask for the same kind of information twice: a shipping address and a billing address, or the buyer's details and a gift recipient's. Two kinds of prefix tell the browser which is which. shipping and billing mark the two standard address roles. A token starting with section-, followed by any name you choose, groups fields into a separate set, so the browser does not fill the recipient's address with the buyer's. The order is fixed: section first, then shipping or billing, then the field name.

The parts of an autocomplete valueThe value section-gift shipping street-address split into three parts. section-gift is an optional name for a group of fields. shipping is an optional role, shipping or billing. street-address is the required field name, which says what the field holds.autocomplete="section-giftshippingstreet-address"optional: a groupyou nameoptional: shippingor billingrequired: what thefield holdsorder is fixed: section, then role, then field name
An autocomplete value reads from general to specific. Most fields need only the last part.
<label for="ship-street">Street address</label> <textarea id="ship-street" name="ship-street" autocomplete="shipping street-address"></textarea> <label for="gift-street">Recipient's street address</label> <textarea id="gift-street" name="gift-street" autocomplete="section-gift shipping street-address"></textarea>

Passwords and codes

Password managers need to know whether a password field is for signing in or for creating a password. current-password on a sign-in form lets them fill the saved password; new-password on a sign-up or change-password form tells them to offer a strong generated one and save it. Put username on the account field beside it, even on a page where the username is shown read-only, so the manager knows which account the password belongs to. one-time-code marks a field for a code sent by text message or email, and some phones, notably iPhones, offer the code that just arrived with one tap. Account forms are covered from the security side in Form Security.

Why autocomplete="off" backfires

Teams often turn autofill off "for security" or to stop browsers putting the wrong thing in a field. It rarely does what they hope:

  • Browsers ignore it where it hurts most. Many browsers do not honor autocomplete="off" on login fields and still offer to save and fill the password, because people with saved, unique passwords are safer than people who reuse a short one they can type.
  • It costs the person. Where it is honored, the person types everything by hand, on a phone, with the typos that brings.
  • It fails an accessibility requirement. WCAG 2.2 success criterion 1.3.5, Identify Input Purpose, asks that fields collecting information about the user identify their purpose in code. The autocomplete tokens are how HTML does that, and the criterion's list of input purposes is the same vocabulary. People with memory or motor impairments depend on it.

The fix for "the browser fills the wrong thing" is the right token, not no token. off is for values that are never worth remembering and are not the person's own data: a code printed on a gift card, the answer to a one-time question, a search over records in an admin tool.

9. Help with typing, and never against it

Capitals, corrections and spelling

Phone keyboards try to help: they capitalize the first letter of a sentence, replace words they think are misspelled, and underline words they do not know. That help is welcome in a message and harmful in a username. Three attributes tune it per field.

  • autocapitalize sets automatic capitals on a virtual keyboard or in voice input: none, sentences, words or characters. words suits a name or a city; none suits a username or a code. It never applies to email, url or password fields, which never capitalize anyway. It does not affect a physical keyboard. It is not Baseline: support is still limited across browsers, so treat it as a refinement that some people get.
  • autocorrect, on or off, controls automatic replacement of what the person typed. Turn it off for usernames, codes, product names and people's names, which autocorrect loves to "fix." It is Baseline newly available as of 2026; Safari had it long before, and the others have now followed. email, url and password fields never autocorrect.
  • spellcheck, true or false, controls the red underline for unknown words. Leave it on for prose, where it helps, and turn it off for codes and names, where every value is an "unknown word." Some browsers offer enhanced spell checking that sends text to a server, so spellcheck="false" is also a reasonable choice on fields holding sensitive identifiers.
<label for="user">Username</label> <input id="user" name="user" type="text" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false">

Never block paste

Some sites stop people pasting into a password field, or into "confirm email," with a script that cancels the paste event. The intention is to make people type carefully. The effect is the opposite. Password managers fill fields by pasting or by simulating it, so blocking paste pushes people toward short, memorable, reused passwords. People with motor or memory impairments, and anyone copying a long code from a message, lose their easiest way in. WCAG 2.2 success criterion 3.3.8, Accessible Authentication (Minimum), says a sign-in step must not depend on a cognitive function test, such as remembering and transcribing a password, unless something helps; its own guidance names password managers and copy-and-paste as that help. Blocking paste removes it. Let people paste everywhere.

The same reasoning answers "type your email twice." A second box catches few typos, because people paste their first answer or repeat the same mistake, and it doubles the work. Show the address back clearly, or send a confirmation link, which proves the address works.

Do not ask twice

A related rule applies across steps. WCAG 2.2 success criterion 3.3.7, Redundant Entry, says information the person already entered in the same process must be filled in for them or offered for selection when it is needed again, unless re-entering it is essential or needed for security. "Billing address same as shipping" as a checkbox, or the shipping address shown and selectable on the payment step, satisfies it.

10. Targets, focus and keys

Big enough to hit

A control is only as easy as it is to hit. WCAG 2.2 success criterion 2.5.8, Target Size (Minimum), asks for pointer targets of at least 24 by 24 CSS pixels, or enough space around a smaller target that a 24-pixel circle centered on it does not touch another target, with exceptions such as links inside a sentence. That is a floor; touch guidelines from phone makers ask for more, around 44 to 48 pixels. Forms meet it most easily with labels: a label is part of its control's target, so a checkbox with a label the full width of the form is a large target even if the box itself is small. Padding on text fields and buttons does the rest.

@layer components { form-field { & :is(input, select, textarea, button) { min-block-size: 2.75rem; padding-inline: 0.75rem; } &:has(> :is([type="checkbox"], [type="radio"])) label { display: inline-block; padding-block: 0.5rem; padding-inline-start: 0.25rem; } & :is([type="checkbox"], [type="radio"]) { inline-size: 1.25rem; block-size: 1.25rem; } } }

autofocus only when the form is the page

autofocus puts focus in a field when the page loads. On a page whose whole purpose is the form, a sign-in page or a search page, that saves a tap. Anywhere else it does harm: it scrolls the page past the content above the form, it makes a screen reader start in the middle of the page, skipping the heading and the explanation, and on a phone it can open the keyboard over the page before the person has read anything. It is a boolean attribute, written as plain autofocus; values such as "true" in old examples mean nothing extra. Use it at most once per page, and only when the form is the reason the page exists.

Leave tab order alone

Keyboard users move through a form with Tab, in source order, which Organizing Forms showed should be the order the form is filled in. The tabindex attribute has three uses, and only two are safe. tabindex="0" makes something focusable that otherwise would not be, in source order; custom widgets need it. tabindex="-1" makes something focusable by script but not by Tab, such as an error summary that a script moves focus to. A positive value is the third use: it pulls the element ahead of everything else on the page, in numeric order, so every field without a number comes after every field with one. One positive value forces the author to number the whole page and keep the numbers in step forever. Old forms advised it; current guidance says never.

Why not accesskey

accesskey assigns a keyboard shortcut to a control. It sounds like a help for keyboard users and mostly is not. Its key combination differs by browser and operating system, so it cannot be explained in a single line; its letters collide with the browser's own shortcuts and with screen reader commands, which some users rely on constantly; nothing on the page shows that the shortcut exists; and a letter chosen for English makes no sense in a translated page. Good labels, a sensible order and Tab serve keyboard users better.

11. An accessibility checklist

The Web Content Accessibility Guidelines, WCAG 2.2, are the standard most accessibility law points to. Their success criteria are numbered and leveled: level A is the minimum, AA is the level most policies require. These are the ones a form meets or fails most often, and where this section covers each.

CriterionLevelWhat it asks of a form
1.3.1 Info and RelationshipsAStructure shown visually is in the markup: labels tied to controls, groups in fieldsets, required in required. (Organizing Forms)
1.3.5 Identify Input PurposeAAFields about the user carry the right autocomplete token. (section 8)
1.4.1 Use of ColorAAn error is not shown by color alone. (section 5)
1.4.4 Resize TextAAText can be zoomed to 200 percent without losing content or function.
2.4.6 Headings and LabelsAALabels and headings describe the field or section they name.
2.4.7 Focus VisibleAAThe focused control can be seen. Never remove the outline without a better one.
2.5.8 Target Size (Minimum)AATargets at least 24 by 24 CSS pixels, or spaced. (section 10)
3.3.1 Error IdentificationAAn error found automatically names the field and describes the problem in text.
3.3.2 Labels or InstructionsAEvery input has a label or instructions, including the format.
3.3.3 Error SuggestionAAWhen the fix is known, say it, unless that would undermine security or purpose.
3.3.4 Error Prevention (Legal, Financial, Data)AAFor purchases, legal commitments and changes to data: reversible, checked, or reviewable before it is final.
3.3.7 Redundant EntryADo not make the person type the same information twice in one process. (section 9)
3.3.8 Accessible Authentication (Minimum)AANo memory or transcription test to sign in without help; allow password managers and paste. (section 9)
4.1.2 Name, Role, ValueAEvery control has an accessible name and exposes its role and state. Native controls do this for free.

Criteria are a checklist, not a test. Two short sessions with the form itself find what a checklist misses.

The keyboard pass

Put the mouse away and fill in the whole form with the keyboard alone. Tab and Shift+Tab move between controls; Space ticks a checkbox and presses a button; arrow keys move within a radio group and a select; Enter submits from a text field. Check that:

  • every control can be reached, in the order the form is filled in, and nothing focusable is invisible;
  • focus is always visible, and never hidden under a sticky header or footer (WCAG 2.2 adds 2.4.11, Focus Not Obscured, for this);
  • nothing traps focus, and nothing moves it unexpectedly while the person is typing;
  • after a failed submission, focus lands on the first problem or on an error summary;
  • custom widgets, if any, work with the keys their role promises.

The screen reader pass

Every major desktop and phone system ships a screen reader: VoiceOver on macOS and iOS, Narrator on Windows, TalkBack on Android, with NVDA a free download for Windows. Turn one on, close your eyes or look away, and fill in the form. Listen for:

  • each control's name, matching its visible label, its role, and "required" where it applies;
  • the group's legend when entering a set of radio buttons or checkboxes;
  • the hint read as a description, and the format said before you type;
  • an error announced when it appears, and read again when you return to the field;
  • after submitting, some clear indication of what happened: success, or where the problems are.

Ten minutes with a screen reader teaches more about a form than an hour with a checklist, and it is the fastest way to find a missing label.

Zoom

People with low vision zoom the page, with the browser's zoom on a desktop or by pinching on a phone. A form must survive it: at 200 percent text size, and in a window 320 CSS pixels wide, which is what 400 percent zoom of a 1280-pixel window amounts to, labels must not overlap fields and nothing must need horizontal scrolling to read (WCAG 1.4.10, Reflow). A single-column form with relative units does this almost by itself.

The one thing that breaks zoom outright is a page that forbids it. A viewport <meta> with user-scalable set to no, or a maximum-scale of 1, tells a phone browser to refuse pinch-zoom. It was often added to stop iPhones zooming in when a field received focus, which they do when the field's text is smaller than 16 pixels. That is an accessibility failure, and Safari on iOS has ignored the setting since iOS 10 in 2016, though other browsers may not. Fix the cause instead: give form fields a font size of at least 1rem.

<meta name="viewport" content="width=device-width, initial-scale=1">

12. Summary

ConceptKey points
Effort and errorsPeople give up when a form takes too much work or scolds too much. Everything on this page serves the person; none of it protects the server, which checks again.
Constraintsrequired, type, pattern (anchored; say the format; escape with the v flag in mind), min/max/step (step defaults to 1), minlength/maxlength (user edits only). badInput means unreadable input.
:user-invalidMatches only after the person has interacted or tried to submit, so nothing is red on load. Baseline widely available. Pair color with a shape and with words.
TimingCheck on leaving a field, recheck as the person types once an error shows, check everything on submit. Progress any time; complaints wait.
MessagesName the field, the problem and the fix, in text, in the field's output.error. Focus the first problem. Keep every value the person typed.
Validation APIvalidity, validationMessage, checkValidity(), reportValidity(), setCustomValidity() (clear it with an empty string), requestSubmit() rather than submit(). formnovalidate for Save draft; novalidate only from a script that takes over.
Keyboardstype picks the keyboard and the rules; inputmode picks the keyboard alone, for digits that are not numbers; enterkeyhint labels Enter.
AutofillThe right autocomplete token on every personal field; section-, shipping, billing for repeated sets; new-password, current-password, one-time-code. Turning it off costs people and fails 1.3.5.
Typing helpautocapitalize, autocorrect, spellcheck off for names, codes and usernames. Never block paste. Never ask for the same thing twice.
Targets and focusTargets at least 24 CSS pixels, labels included. autofocus only when the form is the page. No positive tabindex; no accesskey.
CheckingThe WCAG 2.2 criteria for forms, then a keyboard-only pass, a screen reader pass, and zoom to 200 percent and 320 pixels wide. Never forbid zoom.