RatedWithAI

RatedWithAI

Accessibility scanner

Your form says what went wrong. Does anyone hear it?

The red sentence under a rejected field is the most carefully written text on most websites, and on most websites it is spoken to nobody. It appears after the page loads, in an element nothing is watching, that no field points at — so a screen reader user presses submit and hears silence, with focus still sitting in the box they got wrong. Paste the page your form is on and we will read the four links between their mistake and them being told.

Free, instant, no signup and no card. We read the HTML your server sends, and nothing is stored.

Give us the page the form is actually on — a sign-up, checkout, contact or login page, not the homepage. Free, instant, no signup and no card, and nothing is stored.

The failure that passes every other check

Run a label audit over a typical checkout and it passes. Run a contrast audit: passes. Keyboard: passes. Headings, landmarks, alt text: all fine. Then somebody mistypes their card expiry, presses pay, and the page tells them — in a colour they cannot see, in an element nothing announces, while their focus stays exactly where it was. Nothing in their experience distinguishes that from the button being broken.

It survives every other check because every other check is about the page as it is served. This one is about the page a second after something goes wrong, and the wiring that decides what happens then is invisible on screen: it is four attributes, none of which draw anything. An error message is the only piece of copy on a website whose entire job happens after a mistake, and it is the piece most likely to be built by whoever was closest to the validation library.

The four links, and where yours probably breaks

An error is announced only if all four hold. Every one of them is in the markup before any error exists, which is why this can be read from HTML at all.

1  the field is marked invalid      aria-invalid="true"
2  a message is written             an error container exists
3  the message is tied to it        aria-describedby / aria-errormessage
4  the message is spoken            role="alert" / role="status" / aria-live

   ...and link 4 has to be in the HTML the SERVER sent. A live region
   announces changes INSIDE a region already being watched, so a
   container created at the same moment as its message says nothing.

That last note is the one that catches teams who have already tried to fix this. Adding role="alert" to the element your script builds is the obvious fix, it looks right in the inspector, and on most screen readers it announces nothing at all. The empty container has to be there first.

Five real pages, read live on 25 September 2026

Run against real sign-in and search pages, every result below is a different link in the same chain — which is the argument for reading the chain rather than a score.

github.com/login        2 containers, 2 live regions, 0 fields described
                        breaks at link 3 — nothing points at the message

gov.uk/search/all       22 fields, 7 described, 1 live region
                        2 message containers nothing will announce

wikipedia.org           2 containers holding text, 0 described, 0 live
                        breaks at link 1 and never recovers

news.ycombinator.com    no constraint, no container, no live region
/login                  reported as "cannot tell" — the answer is the next page

basecamp.com            a live region exists, no error container does
                        nothing fails, and nothing proves it works either

The two “cannot tell” results are deliberate. A form that posts and returns a re-rendered page with the error in it satisfies SC 3.3.1 completely, and from outside it is indistinguishable from a form that reports nothing — so the honest answer is to say which fact was established and which was not, rather than to convert a silence into a grade.

Everything this checker reports

Five things it counts as a failure, five it reports without counting against you, and two branches for a page where the answer genuinely cannot be read from HTML.

  • Error message container that nothing will announce

    The error is never announced

    SC 4.1.3 Status Messages (Level AA)

    There is a container on this page whose class or id says it holds an error message, and neither it nor anything wrapping it carries role="alert", role="status" or aria-live. When a script writes the message into it, the text appears on screen and nothing is spoken. A screen reader user who submits the form hears the page go quiet: focus has not moved, no announcement has been made, and the only evidence that anything happened is red text they cannot see. They press submit again.

    The fix: Put role="alert" on the container in the SERVED HTML — empty, before any error exists. A live region only announces changes inside a region the screen reader was already watching, so adding role="alert" at the same moment as the message usually announces nothing. One empty <div role="alert"> per field, or one per form, shipped with the page.

  • Error text on the page that belongs to no field

    The error is never announced

    SC 3.3.1 Error Identification (Level A)

    A container holding what reads as an error message is in the served markup, and no field points at it with aria-describedby or aria-errormessage. Visually it sits under the input it belongs to and the association is obvious. Programmatically the message and the field are two unrelated pieces of the page: moving to the field announces the label and the value, and never the reason it was rejected.

    The fix: Give the message container an id and add aria-describedby="that-id" to the field it describes (or aria-errormessage, alongside aria-invalid="true"). Proximity on screen is not an association — the attribute is.

  • Field marked invalid with no message attached

    The error is never announced

    SC 3.3.1 Error Identification (Level A)

    The field carries aria-invalid="true", so a screen reader announces it as invalid, and there is no aria-describedby or aria-errormessage on it. The person is told that what they typed is wrong and never told what is wrong with it — which is the half of SC 3.3.1 that matters. In practice this is the state a form is left in after a failed submit, so this is what the person hears when they go back through the fields looking for the problem.

    The fix: Add aria-describedby pointing at the element holding the message, at the same time as you set aria-invalid. If the message is the only thing in that element, aria-errormessage is the more precise attribute — it needs aria-invalid="true" alongside it to be exposed at all.

  • Error description pointing at an id that is not on the page

    The error is never announced

    SC 3.3.1 Error Identification (Level A)

    A field's aria-describedby or aria-errormessage names an element id that does not exist in the served HTML. The browser resolves the reference to nothing, so the field has, in effect, no description at all — and it looks correct in the source, which is why this one survives review. The usual causes are a template that renders the message element only when there IS an error, and an id that was renamed on one side.

    The fix: Ship the container with the page, empty, and keep its id stable — that fixes the announcement problem at the same time. If it genuinely cannot exist until an error does, add aria-describedby in the same operation that adds the element, never before.

  • aria-errormessage on a field that is not marked invalid

    The error is never announced

    SC 3.3.1 Error Identification (Level A)

    aria-errormessage is only exposed while the element it sits on is also aria-invalid="true". Without that, the attribute is inert: the message is in the DOM, the reference resolves, and no assistive technology reads it. It is the one ARIA attribute with a hard dependency on another one, and the dependency is easy to miss because everything looks wired up.

    The fix: Set aria-invalid="true" on the field whenever the error is showing, and back to "false" (or remove it) when it is resolved. If you would rather not manage that pairing, use aria-describedby instead — it is announced unconditionally.

  • Live region removed from the accessibility tree

    Not a failure — worth reading

    SC 4.1.3 Status Messages (Level AA)

    A container with live-region semantics also carries the hidden attribute or an inline display:none. Both remove it from the accessibility tree, and a region that is not in the tree is not being watched — so unhiding it and writing the message in are two changes a screen reader may report as one, none or late, depending on the reader. Note this is specifically about hidden and display:none. A visually-hidden live region — sr-only, visually-hidden, the clip pattern — is the recommended technique and is never flagged here.

    The fix: Leave the live region in the tree and empty, and toggle the visibility of the text inside it rather than the region itself. If nothing should be seen when there is no error, an empty region shows nothing anyway.

  • Browser validation switched off, nothing visible replacing it

    Not a failure — worth reading

    SC 3.3.1 Error Identification (Level A)

    The form has the novalidate attribute, which turns off the browser's own checking, and the served markup carries no error container and no live region to replace it. Whatever reports a mistake is therefore built entirely in script and cannot be read from here. That is a perfectly normal way to build a form and it is where the whole failure class lives, because the replacement is written by whoever needed the styling, not by whoever was thinking about announcements.

    The fix: Since you are writing the errors yourself, write the wiring too: an empty role="alert" container shipped with the page, aria-describedby from each field to its own message, and aria-invalid set when the field is rejected.

  • Errors left entirely to the browser's own bubble

    Not a failure — worth reading

    SC 3.3.1 Error Identification (Level A)

    The fields carry real constraints — required, a pattern, a minlength, type=email — and there is no message container and no live region anywhere in the served markup, so the only thing that reports a mistake is the browser's built-in validation bubble. That bubble IS announced, which is why this is not counted as a failure. What it is not is durable: it vanishes on the next keystroke or click, it cannot be re-read, it appears on one field at a time, its wording is the browser's rather than yours, and it is not translated by your site. Somebody who missed it the first time has no way back to it.

    The fix: Keep the native constraints — they are doing real work — and add your own message element per field plus one empty role="alert" region, populated on submit. Then the message persists, reads in your words, and can be found again.

  • Rule the field enforces is stated nowhere until it is broken

    Not a failure — worth reading

    SC 3.3.2 Labels or Instructions (Level A)

    The field has a pattern, a minlength or a maxlength and no description attached to it — no aria-describedby, no title, no hint element. The requirement exists, it is enforced, and the only way to discover it is to fail it. For a password rule or a reference-number format that is several attempts of guesswork, and for somebody using a screen reader it is several attempts with no visible field hint to fall back on.

    The fix: Put the rule next to the field before it is needed — "At least 12 characters" — in an element referenced by aria-describedby. It is the same element the error message can later replace or sit beside.

  • Long form with no summary of what went wrong

    Not a failure — worth reading

    SC 3.3.1 Error Identification (Level A)

    This form has enough fields that a failed submit can reject several at once, and there is no summary container in the markup. Per-field messages alone mean somebody has to walk the whole form to find out which three of fourteen fields were rejected — and for a screen reader user that walk is the only way to find out how many there even were. A summary at the top, focused on submit, turns that into one announcement.

    The fix: Add a container at the top of the form with role="alert" and tabindex="-1", list each error as a link to its field, and move focus to it when the submit fails. This is the GOV.UK error-summary pattern and it is the most tested error pattern in existence.

  • Nothing on this page reports an error — the answer comes back as a new page

    Verify by hand

    SC 3.3.1 Error Identification (Level A)

    The form has no constraint the browser enforces, no message container and no live region, so nothing in the page a person is filling in can tell them they got something wrong. That is not automatically a defect: a form that posts and comes back as a re-rendered page with the error written into it satisfies SC 3.3.1 perfectly well, and it is how a great deal of the web still works. It is also indistinguishable, from here, from a form that says nothing at all. Which of those two this is can only be settled by submitting it wrongly and reading what comes back.

    The fix: Submit the form with something invalid and look at the page that comes back. If the error is in the returned HTML, put it in a container the field points at with aria-describedby and give it role="alert" so it is announced on arrival, then move focus to it. If nothing comes back, that is the Level A failure.

  • No field anybody can get wrong

    Verify by hand

    This page has no form control a person can submit incorrectly — no text field, no select, no checkbox group, nothing beyond at most a button. SC 3.3.1 and 3.3.3 are about input errors, so with no input they do not apply, and the checker reports that rather than inventing findings. If the form you meant to check is behind a click, a route or a login, paste the URL of the page it is actually on.

    The fix: Give the checker the page the form is on — a sign-up, checkout, contact or search page. The site-wide scan crawls to them for you.

Questions

My error messages are right there in red. How can they be a failure?
Because "there" is a visual fact. A screen reader does not read the page the way an eye sweeps it — it reads what it is told to read, when it is told to read it. Text that a script drops into the page after load is not spoken unless it lands inside a region the reader was already watching (that is a live region: role="alert", role="status", or aria-live), or unless focus is moved to it, or unless the field the person is on points at it with aria-describedby. Red text with none of those three is genuinely silent. Submit, nothing, focus still in the field.
Why does it matter whether the live region was in the HTML before the error?
This is the detail that defeats most well-intentioned fixes. A live region works by being watched: the screen reader notes the region when it appears, then announces changes inside it. If your script creates the container AND writes the message in the same operation, most readers see one new element rather than a change inside a watched one, and say nothing. That is why this tool reads for an EMPTY live region in the served HTML. Ship the empty <div role="alert"> with the page and write text into it later — that is the whole difference between working and not.
Is relying on the browser's built-in validation a failure?
No, and this tool will never call it one. The bubble Chrome, Safari and Firefox show for a required or type=email field is announced, so it satisfies SC 3.3.1. It is reported in the review band for four reasons that are about usefulness rather than conformance: it disappears on the next keystroke and cannot be recalled, it shows one field at a time, its wording is the browser's and not yours, and it is not translated by your site. Somebody who looked away when it appeared has no way back to it.
You said a live region behind display:none is a problem. Isn't hiding it the point?
Visually hiding it is the point, and that is a different thing. The sr-only / visually-hidden pattern — clipping the element to a 1px box — keeps it in the accessibility tree, so it is watched and its changes are announced while nothing is drawn on screen. That pattern is correct and this checker never flags it. The hidden attribute and display:none do something else: they remove the element from the tree entirely. A region that is not in the tree is not being watched, so unhiding it and filling it are two changes that may be reported as one, late, or not at all.
What is the difference between aria-describedby and aria-errormessage?
aria-describedby is announced unconditionally: point a field at any element and its text is read after the label and value. aria-errormessage is the more precise attribute — it says "this element is the error for this field" — but it is exposed ONLY while the same field also carries aria-invalid="true". That dependency is the whole of the bug this tool reports as an inert attribute: the markup looks completely correct, the reference resolves, and nothing reads it, because nobody set aria-invalid. If you would rather not manage the pairing, aria-describedby is the safer of the two.
You reported nothing at all on my form. Is it fine?
It means nothing in the served HTML reports an error, which is three different situations that look the same from outside: the form posts and the answer comes back as a re-rendered page (perfectly conformant, and still how a lot of the web works), the whole mechanism is built in JavaScript after load (unknowable from here), or nothing reports anything (a Level A failure). The tool says so rather than guessing. Settle it by submitting the form with something invalid and listening to what happens.
Does the error summary at the top of the form actually help?
It is the most tested error pattern there is. When a submit rejects four of fourteen fields, per-field messages alone mean the person has to walk the entire form to discover which four — and a screen reader user has to do that walk without being able to skim. A summary container at the top with role="alert" and tabindex="-1", focused on submit, turns that into one announcement with links straight to the fields. It is the GOV.UK pattern, and it is why this tool raises the absence of one on longer forms.
Does this replace a real audit of my forms?
No. It reads the wiring, not the wording — and SC 3.3.3 Error Suggestion is about wording: whether the message tells somebody how to fix the thing, not just that it is wrong. "Invalid input" is perfectly wired and useless. It also cannot see error states that only exist after a submit, or focus management, and it reads one page. The site-wide scan renders every page in a real browser, which is where the rest of that lives.
Which criteria is this checking?
Three. SC 3.3.1 Error Identification (Level A): if an input error is detected, the item in error is identified and described in text. SC 3.3.3 Error Suggestion (AA): where a fix is known, suggest it. SC 4.1.3 Status Messages (AA): a message that reports the result of an action, without taking focus, must be available to assistive technology through a role or property. The fourth link of the chain is 4.1.3 almost exactly as written.
Why does a checker for error messages exist separately from your form label checker?
Because they fail separately. A label is what a field is called, and a label audit passes on the great majority of forms that fail this one — the fields are perfectly named, and the message telling somebody the name they typed was rejected is announced to nobody. It is also a different moment: labels matter while you are filling the form in, error handling matters after you have tried and failed, which is the moment people abandon a checkout.

The rest of what a form owes somebody

A field nobody can name fails earlier than this does — the form label checker reads that. A login page that asks somebody to remember or retype something is SC 3.3.8, and a field somebody cannot reach with a keyboard never gets as far as an error at all — the keyboard checker reads that. Because error wiring lives in a component rather than a page, the full WCAG scan is what tells you which of your templates got it right.