Search topics
Inline form errors read out while typing, or not at all
An email field checks every key and sends its error as an alert, read before the address is typed, or shows it on leaving where nothing reads it.
email.addEventListener('input', check); The problem
Screen reader users hear an error about their email address while they are still typing it, or never hear the error they left behind.
A tea shop's checkout asks for an email address first, with a phone number under it. The address is checked in the browser, so a mistake shows under the field before the order is sent: "Enter an email address in the correct format, like [email protected]". Sighted shoppers see it appear in red. What a screen reader user hears depends on when the check runs, and the shop has been built both of the ways many forms are.
In the first, the field is checked on every key, and the message is sent as an alert. A screen reader user types "s", hears the letter, and then, over it, "Enter an email address in the correct format, like [email protected]". They have typed one letter. A few keys later "sam@s" is a valid address, so the message goes; at "sam@shop," it fails again, and the alert is read again, over the letters typed after it. The person can't tell an error about the address they are typing from an error about the address they meant to type.
In the second, the field is checked only when it is left, and the message appears in a plain paragraph under it. The same person types "sam@shop,com", with a comma for the dot, and presses Tab. The screen reader says "Phone number (optional), edit text", and nothing else. The error is on screen, under a field they have left. If they go back, they hear "Email address, edit text, required, sam@shop,com", with no sign that anything is wrong. They find out when the order won't send.
An error that appears without moving focus is a status message: Success Criterion 4.1.3 defines status messages as changes that report the results of an action "or the existence of errors", and asks that they can be presented to people through role or properties without receiving focus. Its Understanding page adds that this should happen "in a way that doesn't unnecessarily interrupt their work", and lists using role="alert" or aria-live="assertive" on content that is not important and time-sensitive as a failure. In the first version, every keystroke runs the check, and each time the message changes it is written into an alert. An error about an address still being typed is neither important nor time-sensitive: it reports a state the person is about to change. The browser does exactly what the page asks: an alert region is read as soon as its text changes, and nothing in it knows the person is in the middle of a word.
In the second version, the message is drawn into a p with no role, so its arrival is a status message that nothing announces, and 4.1.3 fails the other way. The purpose of Success Criterion 3.3.1 is missed too. It asks that, when an input error is detected automatically, the item in error is identified and the error described in text, and its Understanding page says the point is that people are aware an error has occurred and can tell what is wrong. Here the description exists, but only as text drawn under the field. Nothing ties it to the field: there is no aria-invalid and no aria-describedby, so the field is read as if nothing were wrong. The Understanding page doesn't make that markup a requirement of 3.3.1, but without it, or a live region, the only way to learn of the error is to read on past the field line by line, and nobody does that after a Tab that seemed to go fine.
Checkout and sign-up forms are where this is met, often on a phone, where an error under the field may also be behind the on-screen keyboard.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (affected)
- Seizures (not affected)
- Without vision Screen reader users hear an error over the letters they are typing, before the address is finished, and again as it passes and fails while they type on. Where the check waits for them to leave, they hear nothing at all, and the field, when they come back to it, is read with no error.
- Limited language, cognitive and learning abilities An error that flashes up at the first letter, and comes and goes while the address is typed, tells people they've made a mistake they haven't made yet, and pulls their attention off what they are typing.
A scan loads the form and finds a labelled field and an empty paragraph under it. It never types, so it never sees an error appear, let alone when. EaseWeb's audit does type: it enters four letters into an email field and presses Tab, and raises a lead for review when a live region in the form is written while the keys go in, or when new text appears in the form after Tab outside every live region. A person then checks whether that text is the field's error, and what a screen reader says. In the first version a scan may even find markup that looks right: aria-invalid, aria-describedby and role="alert", which is what React Hook Form's accessibility example shows, and what an AI assistant asked to make form errors accessible can add. What's wrong is the moment the code runs, and only typing into the field with a screen reader on shows it.
Try it
Type “sam@shop,com”, comma and all, then press Tab and listen.
Broken 1: the address is checked on every key, and the error is sent as an alert. When is it first read, and how often while you are still typing?
Broken 2: the address is checked when you leave the field. After Tab, is the error read? Press Shift + Tab to go back: is the field read with its error?
Apply a fix. When the field is left: nothing is read while you type, and the error once after Tab. When typing pauses: stop for a second partway through the address.
Broken Can you fix this?
Screen reader says:
(Type in the email field)
The fix
Check when typing is done, and say the error once.
An error checked on every key, sent as an alert
<label for="email">Email address</label><input type="email" id="email" name="email" required aria-describedby="email-error"><p id="email-error" role="alert"></p>…What is wrong3 points
The message is right, and it is tied to the field. When it is said is wrong.
- The check runs on every key, so the error appears at the first letter of an address that isn't finished.
- It comes and goes as the address passes and fails while it is typed, and each time it comes back it is read again.
- It is sent as an alert, which may be read over the letters being typed.
Check when the field is left, and say it once
<label for="email">Email address</label><input type="email" id="email" name="email" required aria-describedby="email-error"><p id="email-error" role="status"></p>…How it is fixed4 steps
- Run the check on
change. It fires when the field is left after its value changed, so nothing is checked while the keys go in, and tabbing past a field nobody typed in marks nothing as wrong. - Give the message's paragraph
role="status", on the page and empty from the start. A polite region asks the screen reader to read the error once, after it has finished what it is saying, usually the next field's name, though that name can cut it short (see Limits). - Point the field's
aria-describedbyat the paragraph, and setaria-invalidin the same step, so going back to the field reads it as invalid, with its error. - Write the message only when it changes, and on
inputonly clear it, once the address passes. Correcting the address takes the error away at once, and nothing new is said while the keys go in.
This fixes both Befores and passes 4.1.3 and 3.3.1. Each error is said once, after the person has finished with the field, without moving focus, and stays tied to the field.
Why this fix: Say it once, when the change has settled
Both pass, and both say an error once, when the person has stopped. Checking when the field is left suits most fields, an email address among them: nobody is helped by hearing that an address is incomplete while they type it, and the error comes when they have said they are done. Checking on a pause suits fields with rules people work towards as they type, such as a new password or a username that must be free: they hear the result each time they stop to see where they are, rather than only after leaving. Many forms use both, field by field, with the same message element.
change, not blur. Keyboard and screen reader users often Tab through a form before filling it in, to learn what it asks. A check on blur runs as they pass each field, and marks every empty required field as wrong before they have typed anything. change fires only when the value has changed. A required field left empty is caught when the form is sent: run the same check then, and move focus to the first field still wrong, where it is read with its error, or show an error summary. See error summaries that don't say which fields failed.
The message has to make sense on its own. It is read after the next field's name, not with its own field, so it names what it is about: "Enter an email address…", not "Invalid format". Form errors that don't say how to fix them covers the wording.
Render the message element with the page. A status region that arrives along with its text is often not read. In a component framework, render the error element empty and keep it mounted, rather than inside an {#if} or a {errors.email && …} that adds it with the message.
When role="alert" is right. W3C's technique ARIA19 writes errors into role="alert" when the form is sent: the person has said they are done, and must act before they can go on. An error found while they are still filling the form in is not that, so it goes to a polite region.
Form libraries. Most libraries choose the moment for you, and the setting is easy to miss.
- React Hook Form checks on submit by default, then on every change after that; its
onBlurandonTouchedmodes check when a field is left, andonTouchedchecks every change after the first time the field is left. Its accessibility example puts the message inrole="alert"; userole="status"and keep the element rendered. - Formik checks on every change and on blur, and its tutorial shows an error only for fields that have been visited. The message element it shows is a plain
divwith no role, tied to nothing. - In any library, write the message element yourself if its own component adds an alert, and give the field
aria-describedbyandaria-invalidfrom the library's error state.
Not before the field is used. Don't render a field with aria-invalid="true" because it is empty and required. Screen readers read "invalid entry" on a field nobody has touched, with no message to explain it. Set it from the check, and only once the check has run.
Not fixes. Moving focus back to the field when it fails stops people leaving it, and keyboard users can be stuck there. Making the message element aria-live="assertive" instead of role="alert" is the same failure. Leaving the message tied to the field but in no live region, and counting on people going back, is the second Before with better markup: nothing tells them to go back.
Character counters are another message that changes as people type, and have the same answer: character counters that stay silent or speak on every key.
Design ideas
An error can look light, and still be said in words, at the right moment.
Four ways to draw a field’s error. Type into each with a mistake, then press Tab or Continue: two say what is wrong in words once you have stopped, and two never put it into words.
In the design file
- Draw each field at rest with its hint, with its error after it is left, and corrected.
- Write beside each error when it shows, either once the field is left or after a one-second pause for a field people work towards, such as a new password.
- Write each error in words under its field, saying what to enter; a red border, an icon or a shake only goes with the words.
- For a checklist, write the one line said when typing stops, such as “2 of 3 rules met”.
- Write the ratio next to the error’s colour, at least 4.5:1 for its words.
Passes
We’ll send your receipt here.
A hint line that becomes the error Under the field, a grey hint says what the address is for, 6.31:1. Leave the field with a mistake and the same line turns red, 6.54:1, with a warning icon and what to enter, and the border thickens to 2 pixels. Nothing below moves, and help and error are always in the same place. The line is said once, as it changes, and again with the field when people come back.Rule of thumb: Say it once, when the change has settled
Meet all 3 rules
- 12 or more characters
- A number
- A capital letter
A password checklist that ticks as you type The rules for a new password are listed under it, each with an empty circle that gets a green tick, 6.35:1, the moment the password meets it, so people see where they are at every key. The line above the list, “2 of 3 rules met”, changes only when they stop for a second, and that line is what a screen reader reads.Rule of thumb: Say it once, when the change has settled
Fails
- A cross or a tick inside the field, and no wordsWCAG 3.3.1 From the first letter, a red cross sits at the end of the field, and it turns into a green tick the moment the address is valid. It looks quick and tidy, but nothing ever says what is wrong or what to enter, and the cross is shown for an address people are still typing. Put the words under the field, and show the cross once the field is left.
- A field that shakes and turns redWCAG 3.3.1 Press Continue with a mistake and the field shakes from side to side and its border turns red, the way a phone’s lock screen says no. The shake shows a change and stops within half a second, so it can stay. But nothing says what is wrong, and with reduced motion only the red border is left. Add the words under the field.
Rule of thumb: Move to show a change, then stop
Verify the fix
4 checks, no mouse.
Test with: Screen reader, Keyboard
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the form library, its settings or the field components change.
How screen readers handle role="alert", role="status", aria-describedby and aria-invalid="true"
Limits
A polite message written as focus moves is read after the next field's name, or cut short by it, depending on the screen reader and how much that field has to say, so the error is also tied to the field, to be read when people return to it. This guide covers one field checked in the browser. What the message says, and how a field shows it is wrong, is covered in form errors that don't say how to fix them; the list of errors when the form is sent in error summaries that don't say which fields failed; and the result of a form sent in place in form success and error messages that are never announced. Test your real form, with its library and its server checks, with the screen readers your users use.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: status-messages (opens in a new tab)
- WCAG Understanding: error-identification (opens in a new tab)
- WCAG Technique ARIA21 (opens in a new tab)
- WAI Tutorial: forms/notifications (opens in a new tab)
- nngroup.com: errors-forms-design-guidelines (opens in a new tab)
- developer.mozilla.org: aria-invalid (opens in a new tab)
EaseWeb service
Fixed it? Keep it from coming back.
I turn guides like this one into rules your coding agents follow and checks your CI runs. Then I retest by hand with a keyboard and screen readers, periodically.