Search topics
Form success and error messages that are never announced
A newsletter form sends in the background and writes "Thanks! Check your inbox to confirm." into a plain paragraph, so the result is on screen and no screen reader hears it.
message.textContent = 'Thanks! Check your inbox to confirm.'; The problem
Screen reader users press Subscribe and hear nothing, so they can't tell whether they signed up, and send it again or give up.
A bookshop's footer has a small newsletter sign-up: "Sign up for our newsletter", an Email address field and a Subscribe button. The form sends in the background, so the page doesn't reload. A second later a line appears under it: "Thanks! Check your inbox to confirm." If the shop's mailing service is down, the same line says "Sorry, we could not sign you up. Please try again." instead. A sighted visitor reads it and moves on. A screen reader user presses Subscribe and hears nothing, whichever way it went. Focus is still on the button, the button still says Subscribe, and the line under it is never read. They press again and get a second confirmation email, or they leave without knowing whether they signed up.
"Thanks! Check your inbox to confirm." is a status message: it tells people the result of what they did, and it doesn't take focus. It is one of the W3C's own examples for Success Criterion 4.1.3: after a user submits a form, text is added to it saying the form was submitted. The criterion asks that such a message can be found by assistive technology through its role or properties, so that it is announced without focus moving. Here the result goes into a plain p, with no role="status" and no aria-live, so the browser has no reason to tell the screen reader that anything changed. That is failure F103. The failure message is a status message too, and fails the same way.
The message sits right under the form, so anyone looking at the form sees it, and only people who rely on what is spoken or shown in braille miss it. When the form is an embed from a mailing-list service, the same silence comes with it onto every page that carries it.
1 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 (not affected)
- Seizures (not affected)
- Without vision Screen reader users press Subscribe and hear nothing, so they can't tell whether they are signed up.
A scan loads the page and finds a form with a labelled field and a named button, and passes it. It doesn't send the form: on a live site that would sign someone up. So it never sees the result appear, and nothing in the empty paragraph says it will hold one. EaseWeb's audit reports a page that has a form and no live region anywhere as a lead to test by hand. A page that has a live region elsewhere, such as a cookie bar, gets no such lead, so the form still has to be sent by a person. AI assistants asked to fix this may add aria-live to the paragraph in the same step that fills it, so the region and its words arrive together. Only sending the form with a screen reader running, once with success and once with failure, shows what is said.
Try it
Press Subscribe and listen.
The panel under the form shows what a screen reader says. Is the result ever heard? Turn on “Sending fails” and press again, twice.
Broken Can you fix this?
Fernhill Books
New titles arrive every Thursday. Open daily, 9 to 6.
Screen reader says:
(Press Subscribe)
The fix
Write the result into a status region that was on the page before the send, and leave focus where it is.
- 1Write the result into a status region that is on the page before the send, and change only its words.
- 2Say both results in words: sent, and not sent.
- 3Leave focus on the button: don't move it to the message, or disable the button while the form sends.
<section aria-labelledby="signup-title"> <h2 id="signup-title">Sign up for our newsletter</h2> <form id="signup"> <label for="signup-email">Email address</label> <input id="signup-email" name="email" type="email" autocomplete="email" required> <button>Subscribe</button> </form> <p id="signup-message"></p></section><section aria-labelledby="signup-title"> <h2 id="signup-title">Sign up for our newsletter</h2> <form id="signup"> <label for="signup-email">Email address</label> <input id="signup-email" name="email" type="email" autocomplete="email" required> <button>Subscribe</button> </form> <p id="signup-message" role="status"></p></section>Why this fix: Show a message next to what it is about
The paragraph is on the page from the start, but it is not a live region, so filling it changes what people see and nothing that is said. Focus stays on Subscribe, which hasn't changed, so there is nothing new for a screen reader to read there either.
The paragraph that was already there gets role="status", so the browser is watching it when its text changes. The screen reader finishes what it is saying, then reads "Thanks! Check your inbox to confirm." Focus stays on Subscribe, so people carry on from where they were. A failure is read the same way. announce() empties the line and writes the words a tenth of a second later, so a second failure with the same words is still a change. Without the pause, an offline request that fails at once could empty and refill the line in the same moment, and the repeat may not be read. The message stays under the form, where everyone who just pressed the button is already looking.
The region must exist first. In a component framework, render the status line with the form, empty, never inside an {#if} that appears with the result. A region that arrives with its message is often not announced; status messages that are shown but never announced shows that failure in full.
Politeness. role="status" waits until the screen reader has finished speaking, and that is enough here: the person has just pressed Subscribe and is waiting for the answer. Keep role="alert" for messages people must act on at once, as alerts that cut off VoiceOver for routine updates explains.
While it sends. Keep the button enabled and ignore presses while a send is on its way. Setting disabled on the focused button can make the browser drop focus to the page, and the person loses their place. If the service can be slow, write "Subscribing…" into the same status line and let the result replace it, as loading spinners that screen readers never announce does with its loading message. Start it from a short timer that the answer cancels, so a quick answer is not announced twice.
Errors in the fields. An address the browser or the server rejects is an error in a field, not a result of the send: it belongs beside the field, tied to it, as in form errors that don't say how to fix them. A longer form that fails with several such errors needs a summary that names every field, as in error summaries that don't say which fields failed; the status line then says only that the form wasn't sent.
A thank-you page. A form that reloads or goes to a new page isn't a status message: the new page's title and heading tell people what happened. In a single-page app, a route change to a thank-you view must be announced as a new view, as single-page app route changes that are never announced covers.
Embedded forms. Forms from mailing-list and marketing services bring their own script and their own success message, and some replace the form without moving focus. Test the embed on your page, and if its script can't be changed, check whether the service offers a setting to redirect to a thank-you page instead.
Not fixes. Moving focus to the message while the form stays on the page does get it read, but it pulls people away from the form they may want to correct, when the status line does the job without moving them. Moving focus is the fix only when the form is gone, as in the guide on forms replaced by a thank-you. Adding aria-live to the paragraph in the same step that writes the message is the same failure as the toast in that guide on status messages: the live region and its words arrive together. A title or an aria-label that changes on the button is not announced reliably and says nothing about failure.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Design ideas
A thank-you can be friendly, and still be heard.
Three ways to show that the sign-up went through. Press Subscribe in each: two say what happened in words a screen reader is also given, and one shows it only as a picture.
In the design file
- Write both messages in full beside the form, the one for sent and the one for not sent.
- Put the message under the button, where people look once they have pressed it.
- Draw what the button shows while the form sends, at the width of its label and still looking like a button.
- Write the ratios next to the message’s colours, at least 4.5:1 for its words.
Passes
- A tinted note that repeats the address A pale teal note with an envelope opens under the field: “Almost done.” in bold, then “We sent a link to [email protected].” in the shop’s ink, 12.40:1. Showing the address lets people spot a typo at once. The words are written into a status line that is there before the press.
Rule of thumb: Show a message next to what it is about
- A button that shows the wait While the service answers, Subscribe keeps its width and teal, 9.08:1, and shows a small turning ring and “Sending…”; the ring stops when the answer comes. The result then appears under the form in the shop’s ink, written into a status line, because a focused button’s new label is read only in some setups.
Rule of thumb: Move to show a change, then stop
Fails
- Confetti and a button that turns into a tickWCAG 4.1.3 Subscribe shrinks to a round teal tick and a burst of confetti flies out and is gone in under a second. It feels like a reward, but the result is never put in words: a screen reader hears nothing, and with reduced motion only the tick is left. Add the sentence to a status line under it and the look can stay.
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 component, its script or the embed changes.
Limits
Live regions behave differently across screen readers and browsers. This pattern covers one short form that sends in place and stays on the page. A design that replaces the form with a thank-you also removes the button that had focus, so a status line alone is not enough there: focus has to move to the thank-you, as forms replaced by a thank-you that is never announced shows. Actions that aren't forms, such as add to cart, are covered in Add to cart buttons that change a badge and say nothing, and search results that update in place in search and filter results that update without a word. Test your real forms, including embedded ones, with both a success and a failure and 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.
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.