Accessible Form Validation & Error Handling
Error handling is where most forms fail their users. This guide covers the part every checklist glosses over: when to validate, how to tie an error to its field with aria-invalid and aria-describedby, the error-summary pattern and its focus move, announcing inline errors through live regions without double-speaking, suggesting the fix, preventing the costly mistake, and WCAG 2.2's Redundant Entry and Accessible Authentication — with copy-ready code mapped to WCAG 3.3.1 through 3.3.9.
Where Forms Fail Their Users
A form that everyone can fill in and no one can recover from is still a broken form. The moment a submission fails, an invisible contract is tested: does the interface tell the user that something went wrong, which field, and how to fix it — in a way a screen reader will read, a keyboard will reach, and a colour-blind user will see? Most forms answer with a red border and nothing else, and that is a red border away from useless.
This is the deep dive on the error side of forms. If you are building a form from scratch — labels, structure, grouping, autocomplete — start with the accessible forms guide, which covers the whole control. This guide zooms in on the single most-failed part of it: the validation and error-handling lifecycle, from the moment you decide when to check a field to the confirmation step that keeps a user from an irreversible mistake.
Nearly all of it lives in one cluster of the standard — the WCAG 3.3 “Input Assistance” guidelines, success criteria 3.3.1 through 3.3.9 — plus 4.1.3 Status Messages for the announcements and 1.4.1 Use of Color for never relying on red alone. We will take them in the order a real form meets them.
The WCAG 2.2 Criteria Your Validation Must Satisfy
| Criterion | Level | What your form must do |
|---|---|---|
| 3.3.1 Error Identification | A | Every error is described in text and programmatically tied to the field that caused it — not shown by colour or an icon alone. |
| 3.3.2 Labels or Instructions | A | Fields carry visible labels and any format or requirement is stated up front, so many errors never happen in the first place. |
| 3.3.3 Error Suggestion | AA | When a correct value is known, the message suggests it — the expected format or a valid option — rather than only saying something is wrong. |
| 3.3.4 Error Prevention (Legal, Financial, Data) | AA | Submissions with legal or financial weight, or that change user data, are reversible, checked, or confirmed before they take effect. |
| 3.3.7 Redundant Entry | A | Information the user already gave earlier in the same process is not asked for again, unless re-entry is essential. |
| 3.3.8 Accessible Authentication | AA | Logging in does not depend on remembering a password, transcribing characters, or solving a puzzle without an accessible alternative. |
| 4.1.3 Status Messages | AA | Error counts and success confirmations are announced to a screen reader without moving focus — through a live region. |
| 1.4.1 Use of Color | A | An error is never signalled by colour alone; a text message and aria-invalid carry the meaning that red only reinforces. |
Two more sit above these at Level AAA and are worth reaching for: 3.3.6 Error Prevention (All) extends the reversible-checked-confirmed safety net to every submission, and 3.3.9 Accessible Authentication (Enhanced) removes the object-recognition exception from 3.3.8. The criterion forms fail most often is 3.3.1, because the error lives in a CSS class and never reaches the accessibility tree at all.
1. When to Validate: Timing Is the Whole Game
Before a single line of ARIA, decide when your form checks a field. Get this wrong and no amount of correct markup saves you — you will announce errors for fields the user is still typing, or say nothing until it is too late. There are three moments you can validate, and the accessible answer is not to pick one but to sequence them.
The three moments, and what each costs
On every keystroke
Hostile by default. Flags an email as invalid while it is half-typed and, with a live region, machine-guns “invalid” at a screen reader user. Reserve it for feedback the user asked for — a strength meter — and debounce it.
On blur
Gentler, still premature. Fires the first time someone tabs out of an empty required field they fully intend to return to, and can trap a keyboard user who is only passing through.
On submit
The one honest moment. It is the only point you can be sure the user has finished. Always validate here — and make it the anchor the other two moments defer to.
The pattern that respects everyone
Validate fully on submit. Then, and only for a field already in an error state, re-check it as the user edits so the error clears the moment it is fixed. No field is ever marked wrong before the user has tried to send the form, and no corrected field keeps a stale error. This is sometimes called reward early, punish late.
const form = document.querySelector("form")
let hasSubmitted = false
form.addEventListener("submit", (event) => {
hasSubmitted = true
const errors = validate(form) // your rules -> [{ field, message }]
if (errors.length > 0) {
event.preventDefault()
showErrors(errors) // per-field messages + summary (sections 2-3)
focusSummary() // move focus so it is announced once
}
})
// Only start live-validating AFTER the first failed submit,
// and only to clear an error the user is actively fixing.
form.addEventListener("input", (event) => {
if (!hasSubmitted) return
const field = event.target
if (field.getAttribute("aria-invalid") === "true") {
revalidateField(field) // remove the error the instant it passes
}
})The best error is the one that never happens. Before you tune validation timing, prevent the error at the source — a visible label, a stated format, and a sensible input type and autocomplete token satisfy 3.3.2 Labels or Instructions and quietly delete whole categories of mistake.
2. Tie the Error to Its Field
This is the heart of 3.3.1, and it is three moves, none of which is a colour. Mark the field invalid, write the error as real text, and connect the two so a screen reader reads the message when focus reaches the field.
<label for="email">Email address</label>
<!-- A hint stated up front prevents errors (3.3.2). -->
<p id="email-hint" class="hint">We'll email your receipt here.</p>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
aria-invalid="true"
aria-describedby="email-hint email-error" <!-- hint first, then error, in reading order -->
/>
<!-- Real text, not an icon or a colour. This is the error itself. -->
<p id="email-error" class="error">
Enter an email address in the format name@example.com.
</p>Because aria-describedby lists the error's id, a screen reader reads the message every time the field gains focus. aria-invalid="true" reports the field as invalid so the user hears “invalid” on the control itself. When the field is corrected, set aria-invalid to "false" and drop the error id from aria-describedby (or remove the error node) so the description and the state never contradict each other.
required, aria-required, and the native-validation decision
Use the native required attribute on real form controls — it sets the invalid state for free and is exposed to assistive technology without any ARIA. Reserve aria-required="true"for custom widgets that cannot take the native attribute. The browser's own constraint validation (required, type="email", pattern) is an asset, but its default error bubbles are inconsistent and poorly announced. The usual move is to keep the constraint attributes as documentation and a no-JavaScript fallback, add novalidate to the <form>, and take over the messaging yourself using the same validity state:
<form novalidate> <!-- keep the attributes, control the messages -->
...
</form>
// The Constraint Validation API still detects the problems for you:
if (!emailInput.validity.valid) {
if (emailInput.validity.valueMissing) message = "Enter your email address."
else if (emailInput.validity.typeMismatch) message =
"Enter an email address in the format name@example.com."
}aria-errormessage exists — but aria-describedby is the safer default. ARIA has a purpose-built pairing: aria-invalid="true" plus aria-errormessage pointing at the message. It is semantically precise, but screen-reader and browser support has lagged, and the message is only exposed while aria-invalid is true. Until support is universal, aria-describedby is read more reliably across the board — which is why the examples here use it. If you adopt aria-errormessage, test it in your target screen readers first.
3. The Error Summary: The On-Submit Workflow
Per-field messages are read when the user reaches each field — but on submit, a keyboard or screen reader user is often dropped back at the top of a long form with no idea what failed. The error summary fixes that: a block at the top, rendered after a failed submit, that names every problem as a link to the field that caused it. It is the pattern the GOV.UK Design System popularised, and it is the most reliable way to satisfy the on-submit half of 3.3.1.
<!-- Rendered at the top of the form only after a failed submit. -->
<div class="error-summary" id="error-summary" tabindex="-1">
<h2 class="error-summary__title">There is a problem</h2>
<ul>
<li><a href="#email">Enter an email address in the format name@example.com</a></li>
<li><a href="#password">Password must be at least 12 characters</a></li>
</ul>
</div>function focusSummary() {
const summary = document.getElementById("error-summary")
summary.focus() // tabindex="-1" makes the container programmatically focusable
}
// Each summary link jumps to its field by matching id; also focus the field
// so the landing is reliable across browsers.
summary.addEventListener("click", (event) => {
const link = event.target.closest("a")
if (!link) return
const field = document.querySelector(link.getAttribute("href"))
if (field) { event.preventDefault(); field.focus() }
})Announce the summary once — pick one channel. Moving focus to the summary (via tabindex="-1") makes a screen reader read it because focus landed there, and it scrolls the summary into view for sighted keyboard users. Adding role="alert" makes it announce as a live region too — so a summary that both takes focus and carries role="alert" is read twice. Move focus or use role="alert", not both. The focus move is usually the better choice because it also positions the user to act.
Keep each summary item's wording identical to the field's own error message so the user is not solving two different riddles, and order the items to match the visual order of the fields. The summary and the per-field messages are two views of the same data — generate both from one list of errors so they can never disagree.
4. Announce Inline Errors Without Stealing Focus
Sometimes an error or a status appears without a submit and without moving focus — a debounced “username is taken”, a “saved” confirmation, a running count of problems. Those changes must reach a screen reader through an ARIA live region, which is exactly what 4.1.3 Status Messages requires.
<!-- One shared, always-present region. It must exist in the DOM BEFORE
you write to it, or the first message is missed. -->
<div id="form-status" aria-live="polite" class="sr-only"></div>
// Writing text into it announces that text, without moving focus.
document.getElementById("form-status").textContent =
"2 problems found. See the list at the top of the form."Use aria-live="polite" for almost everything — it waits for the screen reader to finish its current sentence. Reserve aria-live="assertive" (or role="alert", which implies it) for a message the user must hear immediately, because it interrupts. A validation error the user just caused is usually fine to announce politely.
The double-announcement trap. Do not make a per-field error node both an aria-describedby target and an aria-live region. When you populate it, the live region announces it once; then focus reaching the field announces it again through the description — every error, doubled. Keep the roles separate: aria-describedby for on-focus reading, one shared polite region for on-change announcements. If you already move focus to a summary or the first invalid field on submit, the error nodes need no live behaviour at all.
Live regions are their own small discipline — atomic updates, clearing before re-announcing, role="status" versus role="alert". The Status Messages guide covers the full set of rules and the toast, result-count, and loading-state patterns that build on them.
5. Suggest the Fix, and Prevent the Costly Mistake
Identifying an error is the floor, not the ceiling. Two more criteria ask the message to help, and the form to protect the user from a mistake that is expensive to undo.
3.3.3 Error Suggestion: say how to fix it
When you can determine a correct value, the message must offer it — the expected format, the valid range, the nearest match — not merely announce that the input is wrong. “Invalid date” fails; “Enter a date as DD/MM/YYYY, for example 09/06/2026” passes. The one exception is where revealing the suggestion would undermine security or purpose — a login form should say “Your email or password was incorrect” rather than confirming which half was right.
3.3.4 Error Prevention: reversible, checked, or confirmed
When a submission carries legal weight, moves money, or changes or deletes data the user controls, at least one of three safeguards must be in place. This is not screen-reader-specific — it protects users with cognitive disabilities, motor disabilities, and everyone else from an irreversible slip.
Reversible
The submission can be undone — a cancellation window on an order, a “delete” that trashes rather than destroys.
Checked
Input is validated for errors and the user gets a chance to correct them before the submission takes effect.
Confirmed
A review step shows everything the user entered and asks them to confirm before it is final — the classic checkout “Review your order” page.
A review-and-confirm step is the most common answer and the most useful, because it doubles as a place to read back every value one last time. If you can, aim for 3.3.6 Error Prevention (All) and extend the same safety net to every submission, not just the legal and financial ones.
6. Do Not Ask Twice: Redundant Entry
3.3.7 Redundant Entry (Level A, new in WCAG 2.2) is a cognitive-accessibility requirement that reads like plain courtesy: within a single process, do not make the user enter the same information twice. Re-typing an address you gave two steps ago is exactly the memory-and-transcription load that trips up users with cognitive disabilities — and a friction point for everyone on a phone.
The criterion allows three exceptions: when re-entry is essential (confirming a password, a deliberate security re-check), when the earlier information is no longer valid, or when auto-populating it would undermine security. Everything else should be carried forward. Practical techniques:
- Offer a “same as billing address” control that copies the earlier values rather than asking for them again.
- Carry data between the steps of a multi-step form, and show it back to the user rather than making them re-key it.
- Use
autocompletetokens (autocomplete="email","street-address","tel") so the browser and password managers can fill values the user has stored — which also helps 1.3.5 Identify Input Purpose. - Where a value must be re-selected rather than re-typed, let the user pick it from what they entered before instead of recalling it.
The test is simple: complete your own multi-step flow and count how many times you type the same fact. Every count above the essential ones is a 3.3.7 problem.
7. Accessible Authentication
The login step is a form too, and 3.3.8 Accessible Authentication (Minimum) (Level AA, new in WCAG 2.2) governs it. The rule: a login process must not rely on a cognitive function test — remembering a password, transcribing characters, solving a puzzle, or reading distorted text — unless an accessible alternative is offered, a mechanism helps complete it, or the test is object- or personal-content recognition.
The most common way sites fail it is by fighting the very tools that make authentication accessible. Fixes are mostly a matter of getting out of the way:
- Allow paste into password fields. Never attach an
onpastehandler that blocks it — it breaks password managers and forces recall and retyping, the exact burden the criterion forbids. - Keep
autocompleteon. Useautocomplete="current-password"and"new-password"so browsers and managers can fill and generate credentials. - Support passkeys / WebAuthn. A biometric or device passkey is authentication with nothing to remember — the cleanest way to pass, and increasingly expected.
- Offer an email or SMS link, or OAuth. A magic link or a “sign in with” button removes the recall step entirely.
- Drop transcription CAPTCHAs. Reading distorted characters is a cognitive-function test. Object-recognition (“select the buses”) and personal-content checks are the allowed exceptions at AA — but 3.3.9 (Enhanced, AAA) removes even the object one.
The through-line of 3.3.8 is that the user should not have to remember or retype a secret. If your login can be completed by a password manager, a passkey, or a link in an email, you have satisfied it — and made the experience better for everyone at the same time.
8. Validation in React
In React the state moves into hooks, but the accessibility contract does not change: aria-invalid, a text message, and aria-describedby tying them together. Use useId to generate the paired ids so a component can appear more than once on a page without collisions.
function EmailField({ value, onChange, error }) {
const id = useId()
const hintId = id + "-hint"
const errorId = id + "-error"
const invalid = Boolean(error)
return (
<div>
<label htmlFor={id}>Email address</label>
<p id={hintId} className="hint">We'll email your receipt here.</p>
<input
id={id}
type="email"
value={value}
onChange={onChange}
autoComplete="email"
required
aria-invalid={invalid || undefined}
aria-describedby={invalid ? hintId + " " + errorId : hintId}
/>
{invalid && (
<p id={errorId} className="error">{error}</p>
)}
</div>
)
}Note aria-invalid={invalid || undefined}: passing undefined removes the attribute entirely when the field is valid, rather than leaving aria-invalid="false" hanging around. A library such as React Hook Form or Formik manages the error state for you, but it does not wire the ARIA — you still own aria-invalid, aria-describedby, the summary, and the focus move. React Aria's form and field hooks go further and handle the wiring for you. Whichever you choose, verify it against the workflow below. The same principles carry to the other frameworks — see the React, Vue, and Angular accessibility guides.
How to Test Accessible Form Validation
A scanner can confirm an input has a label; it cannot tell you whether a user can recover from a failed submit. That is a hands-on test, and it takes a couple of minutes.
- Submit it broken, with the keyboard and a screen reader. Leave every field wrong and press the submit button. Focus should move to an error summary or the first invalid field, and the screen reader should announce that the submit failed and how many problems there are — not leave you silent at the top.
- Tab through the invalid fields. Each should announce its label, that it is invalid, and its specific message — proof that
aria-invalidandaria-describedbyare wired. Use the commands in the NVDA and VoiceOver guides. - Fix one field and listen. The error should clear as soon as the value is valid, and it should not be re-announced on every keystroke — the reward-early, punish-late timing from section 1.
- Turn off colour. Switch the display to greyscale and confirm you can still tell which fields failed and why — the meaning is in text, not in red (1.4.1).
- Read the messages aloud. Do they say what to do, or only that something is wrong? Every message should suggest the fix where one is known (3.3.3).
- Try to break the login. Paste into the password field, let a password manager fill it, and check nothing blocks either — and that no character-transcription CAPTCHA stands in the way (3.3.8).
Layer axe-core on top for the mechanical checks — see automated vs manual testing — and scan the live page with the URL accessibility auditor to catch a missing name or an unassociated error before it ships.
Common Validation Mistakes & How to Fix Them
| Anti-pattern | Why it fails | The fix |
|---|---|---|
| The only sign of an error is a red border or a red asterisk. | Colour alone is invisible to a screen reader and to many colour-blind users, so they cannot tell which field failed or why (1.4.1, 3.3.1). | Add a text message, set aria-invalid="true", and tie the message to the field with aria-describedby. Colour becomes a redundant extra cue, not the only one. |
| Validation fires on every keystroke, flagging fields the user is still typing. | The user is told their half-typed email is invalid, and a screen reader user hears "invalid" repeatedly — noise that punishes people for making progress (3.3.1, 4.1.3). | Validate on submit; only re-check a field live after the first failed submit, and only to clear its error once it is fixed. |
| On submit the page reloads or updates but focus stays put and nothing is announced. | A keyboard or screen reader user has no idea the submit failed, what went wrong, or where to fix it — they are stranded (3.3.1, 4.1.3, 2.4.3). | Render an error summary, move focus to it (tabindex="-1"), and link each item to its field. Announce it once, through the focus move or role="alert", not both. |
| The error message just says "Invalid" or "This field is required." | It identifies that something is wrong but not how to fix it, which fails 3.3.3 when a correct value is known and is a barrier for users who cannot guess the expected format. | Describe the fix: "Enter an email in the format name@example.com" or "Password must be at least 12 characters." Suggest a value whenever you can determine one. |
| The per-field error node is both an aria-describedby target and an aria-live region. | It is announced twice — once as a live change, once when focus reaches the field — so the user hears every error doubled (4.1.3). | Use aria-describedby for on-focus reading and a single shared polite live region for on-change announcements. One element should not do both jobs. |
| The password field blocks paste and disables autocomplete, and login hides behind a character CAPTCHA. | It forces the user to recall and retype a secret and pass a cognitive-function test, blocking password managers and failing 3.3.8 Accessible Authentication. | Allow paste and autocomplete, support passkeys/WebAuthn or a magic link, and replace transcription CAPTCHAs with an allowed check or an accessible alternative. |
Accessible Form Validation Checklist
- Right timing. Full validation on submit; live re-checking only after a failed submit, and only to clear a field the user is fixing — never on every keystroke.
- Error tied to field.
aria-invalid="true", a real text message, andaria-describedbyconnecting them (3.3.1). - Not colour alone. Every error survives greyscale — the meaning is in text, red is a redundant cue (1.4.1).
- Summary with focus. An error summary on submit, focus moved to it (
tabindex="-1"), each item a link to its field — announced once, not twice. - Announcements, not double-speak. On-change statuses go through one shared polite live region; no node is both an
aria-describedbytarget and a live region (4.1.3). - Messages suggest the fix. Expected format or a valid value where one is known, not just “invalid” (3.3.3).
- Costly actions protected. Legal, financial, or data-changing submissions are reversible, checked, or confirmed (3.3.4).
- No redundant entry, accessible login. Data carried forward across steps (3.3.7); paste, autocomplete, and passkeys allowed, no transcription CAPTCHA (3.3.8).
Work through the full WCAG 2.2 checklist to see form validation in the context of every other requirement, and the accessible forms guide for labels, grouping, and structure.
Check Your Form on a Live Page
Scan any page with our free axe-core-powered auditor to catch an input with no label, an error that is not associated with its field, or a validation state that lives only in CSS — then run the broken-submit test above for the failures no scanner can see.
Frequently Asked Questions
When should a form validate — on submit, on blur, or as the user types?▾
Validate on submit for everyone, because it is the one moment you can guarantee the user has finished. Validating on every keystroke is hostile: it flags an email address as invalid while the user is halfway through typing it, and a screen reader user hears a stream of "invalid" announcements. Validating on blur is gentler but still fires the first time someone tabs through an empty required field they fully intend to fill in. The pattern that respects everyone is a hybrid: run full validation when the form is submitted, and only after that first failed submit start re-checking an individual field as the user edits it — so the error clears the instant it is fixed, but no field is ever marked wrong before the user has tried to send the form. Reserve genuinely live feedback (a password-strength meter, a username-availability check) for cases where the user expects it, debounce it, and announce it through a single polite live region rather than firing on each character.
How do I tie an error message to the field it belongs to?▾
Do three things to every field in error, and a red border is none of them. Set aria-invalid="true" on the control so assistive technology reports it as invalid; render the error as real text (not an icon or colour alone) so it survives 1.4.1 Use of Color; and point the field's aria-describedby at that message's id so a screen reader reads the error whenever focus reaches the field. If the field also has a hint, list both ids in aria-describedby in reading order — hint first, then error. When the field is corrected, set aria-invalid to "false" (or remove it) and remove the error text so the two never drift out of sync. This wiring is what satisfies 3.3.1 Error Identification: the error is identified in text and programmatically associated with the field, not just shown as a visual style.
What is the error summary pattern, and why use it?▾
An error summary is a block at the top of the form, rendered after a failed submit, that lists every error as a link pointing to the field that caused it. It solves a problem an inline-only approach does not: a keyboard or screen reader user who submits a long form and lands back at the top has no idea what went wrong or where. The summary gives them the full count and, because each item is a link to the field's id, a one-key jump to fix it. The load-bearing detail is focus: give the summary tabindex="-1" and move focus to it on submit, so the user is taken to the list and a screen reader reads it. Move focus to the summary or use role="alert" on it, but not both — doing both announces the summary twice. This is the on-submit half of 3.3.1; pair it with the per-field aria-describedby messages for when the user reaches each field.
Why do my form errors get announced twice by a screen reader?▾
The usual cause is making the same element do two jobs: the per-field error node is both the target of aria-describedby and an aria-live region. When you populate it, the live region announces it once, and then focus reaching the field announces it again through the description. Pick one channel per moment. On submit, move focus to the error summary (or the first invalid field) — focus landing there reads the message once, so the individual error nodes do not need to be live regions at all. For feedback while the user types, use a single dedicated polite live region for the whole form rather than marking every error paragraph aria-live. The rule of thumb: aria-describedby is read when focus arrives; aria-live is read when content changes; if an element has both, you get both.
What is WCAG 3.3.7 Redundant Entry?▾
Redundant Entry (Level A, new in WCAG 2.2) says that within a single process — a checkout, a multi-step application — you must not ask the user to re-enter information they already provided in that same process, unless re-entering it is essential, the earlier information is no longer valid, or auto-populating it would undermine security (a password confirmation is the classic exception). It is a cognitive-accessibility requirement: re-typing an address you gave two steps ago is exactly the kind of memory-and-transcription burden that trips up users with cognitive disabilities, and it is a friction point for everyone. Satisfy it by carrying data forward between steps, offering a "same as billing address" control, auto-populating from earlier answers, and using autocomplete tokens so the browser can fill known values. The simplest test: complete your own multi-step flow and count how many times you type the same fact.
What does WCAG 3.3.8 Accessible Authentication require?▾
Accessible Authentication (Minimum) (Level AA, new in WCAG 2.2) says a login step must not depend on a cognitive function test — remembering a password, solving a puzzle, transcribing characters, or identifying them from a distorted image — unless an accessible alternative is provided, the test can be completed with the help of another mechanism, or it is object- or personal-content recognition. In practice that means: let the browser and password managers fill the password field (never block paste with onpaste handlers, and never disable autocomplete on password inputs), support passkeys and WebAuthn or an email or SMS magic link so the user is not forced to recall a secret, and drop character-transcription CAPTCHAs in favour of the allowed exceptions or a non-cognitive check. The enhanced version, 3.3.9 (Level AAA), removes even the object- and personal-content-recognition exceptions. The core idea is that authenticating should not require the user to remember or retype anything.
Should I turn off the browser's native form validation?▾
Native constraint validation (required, type="email", pattern, and the browser bubbles) is a real accessibility asset — it works with no JavaScript and is keyboard-operable — but the default error bubbles are inconsistent between browsers, vanish quickly, are hard to style, and are not reliably announced by every screen reader. The common approach is to keep the constraint attributes on your inputs (they document intent and act as a fallback) but add novalidate to the <form> so you can take over the messaging with the Constraint Validation API, rendering your own text errors, aria-invalid, describedby, and summary. That gives you a consistent, well-announced, styleable experience while the underlying validity state (checkValidity(), validity.valueMissing, and friends) still does the detection work for you. If you have no JavaScript at all, leave native validation on — imperfect built-in errors beat none.
How do I test whether my form errors are accessible?▾
Submit the form with the keyboard alone and a screen reader running, and leave every field wrong. Focus should move to an error summary or the first invalid field, and the screen reader should announce that something failed and how many — not leave you silent at the top of the page. Tab through the fields: each invalid one should announce its label, that it is invalid, and its specific error message, because aria-invalid and aria-describedby are wired up. Fix one field and confirm the error clears and is not re-announced on a loop. Turn the screen off or set the display to greyscale and confirm you can still tell which fields are wrong — errors must not rely on colour (1.4.1). Check that the error text says what to do ("Enter a date as DD/MM/YYYY"), not just that something is wrong (3.3.3). Then layer axe-core on top for the mechanical checks, but the keyboard-and-screen-reader submit is the test that decides whether a user can actually recover.
Essential Accessibility Resources
Comprehensive tools, checklists, and guides to help you create inclusive digital experiences