Testing Form Errors with NVDA
NVDA is a free, open-source screen reader for Windows and one of the most widely used screen readers in the world, which makes it the natural first choice for testing form validation. It is also approachable: a developer can install it, learn half a dozen keys and run a meaningful test of a form’s error handling in about twenty minutes. This walkthrough covers setting NVDA up for testing, the Speech Viewer that turns announcements into text you can copy, the difference between browse mode and focus mode that explains most confusing results, and a concrete pass through a form with errors — what you should hear at each step and what the common failures sound like. It applies the method from screen reader testing for forms.
The failure this guide prevents: shipping error handling that looks complete but that NVDA users hear as silence, or as the same message repeated on every keystroke.
Prerequisites
| Requirement | Notes |
|---|---|
| Windows 10 or 11 | A virtual machine works well |
| NVDA (current release) | Free from NV Access |
| Firefox or Chrome | Test the browser your users use most |
| Headphones | Or speakers at a comfortable volume |
| The form’s test script | Steps and expected announcements |
Step 1: Set Up NVDA for Testing
Install NVDA and start it with Ctrl+Alt+N. The NVDA key is Insert by default (Caps Lock can be enabled as well); commands below write it as NVDA. Open the NVDA menu with NVDA+N, then Tools → Speech Viewer. Speech Viewer shows every phrase NVDA speaks, in order, as text. Keep it open beside the browser for the whole session.
Leave speech settings at their defaults: default verbosity is what most users hear. The one change worth making for a testing session is a slightly faster speech rate so you do not wait too long — but not so fast that you miss announcements. Turn NVDA off with NVDA+Q when you finish.
Step 2: Understand Browse Mode and Focus Mode
NVDA has two ways of interacting with web pages, and most surprising test results come from not noticing which one you are in:
- Browse mode is for reading. Arrow keys move through content, and single letters jump between elements: F to the next form field, H to the next heading, E to the next edit field.
- Focus mode is for interacting with controls. Keys go to the control, so typing enters text.
Pressing Tab onto a text field normally switches to focus mode automatically, with a soft higher-pitched sound; pressing Escape returns to browse mode, and NVDA+Space toggles manually. Live region announcements are spoken in both modes. aria-describedby descriptions are spoken when a field receives focus, and in browse mode when you land on it. Test both: many users move through forms with F in browse mode, not only with Tab.
Step 3: Walk Through the Form
Use this example form: an email field with required and type="email", a postcode field with required, a Continue button, and an error summary that receives focus on a failed submit. The expected announcements below are typical for NVDA with Firefox; wording varies slightly by version and browser.
Focus the email field (Tab). Expect the label, the role, “required” and any hint: “Email address, edit, required, We’ll send your receipt here.” If the hint is missing, check aria-describedby. If “required” is missing, check that the required attribute (or aria-required="true") is present.
Type an invalid value and Tab away. With on-blur validation that only uses aria-describedby, you hear the next field’s label and nothing about the email error. That is expected: the error is heard when you return. If a polite live region is used as well, the error follows the next field’s label.
Return with Shift+Tab. Expect: “Email address, edit, invalid entry, required, Enter an email address like name@example.com.” “Invalid entry” comes from aria-invalid="true"; the message comes from aria-describedby. If you hear the hint but not the error, the error element’s id is missing from aria-describedby. If you hear “invalid entry” with no explanation, the message is not linked at all.
Leave the postcode empty and activate Continue. Expect focus to move to the summary and NVDA to read its heading: “There are 2 problems, heading level 2.” Then press Tab or Down arrow to reach the links. If you hear only “Continue, button” again, focus did not move — the most common and serious failure.
Follow the first summary link. Expect focus in the email field with its error read, as on return. If the link moves the page but not focus, NVDA will say nothing useful; the link’s target must be the input’s id so focus lands in it.
Fix both fields and submit. Expect a clear success announcement or a new page title. Silence after a successful submit leaves users unsure whether it worked.
Step 4: Record What You Heard
Copy the relevant lines from Speech Viewer into your results table. Exact text makes bugs reproducible and makes a wording change visible in the next release:
| Step | Expected | Speech Viewer text | Result |
|---|---|---|---|
| Return to email | invalid entry + message | “Email address edit invalid entry required Enter an email address like name@example.com.” | Pass |
| Submit with errors | summary heading | “Continue button” | Fail — focus not moved |
The Markup NVDA Needs
<label for="email">Email address</label>
<input id="email" name="email" type="email" required
aria-invalid="true" aria-describedby="email-hint email-error">
<p id="email-hint">We'll send your receipt here.</p>
<p id="email-error">Enter an email address like name@example.com.</p>
function showError(input: HTMLInputElement, message: string): void {
const err = document.getElementById(`${input.id}-error`)!;
err.textContent = message;
err.hidden = message === "";
input.setAttribute("aria-invalid", String(message !== ""));
}
Keeping the error element in the DOM and changing its text avoids a subtle NVDA problem: if the element is created after the input already has focus, the new description is not read until focus leaves and returns. The pattern is explained in linking errors with aria-describedby.
Common Failures and Their Fixes
Nothing announced after a failed submit. Focus did not move and no live region spoke. Move focus to the summary heading (with tabindex="-1") or to the first invalid field.
“Invalid entry” on page load. aria-invalid="true" is set before the user has interacted. Only set it after a blur or submit.
Error announced on every keystroke. A role="alert" element is updated on input. Update the visual message while typing if you must, but announce only on blur or submit.
Hint read, error not read. aria-describedby lists only the hint id. Include both ids, hint first.
Native browser bubble instead of your message. The form lacks novalidate, so the browser’s own validation ran first. NVDA reads the native bubble inconsistently across browsers; use novalidate with your own messages, keeping reportValidity() only if you have chosen native bubbles deliberately.
NVDA with Chrome versus Firefox
NVDA works well with both browsers, but the two expose some details differently, so results from one do not guarantee the other. Descriptions may be read with slightly different pauses and punctuation, the order of “required” and “invalid entry” can differ, and focus-mode switching on custom widgets can behave differently. If your analytics show significant use of both, run the script in each at least once per release, and record the browser version with every result.
Checking the Elements List
NVDA+F7 opens the Elements List, which can show form fields on the page with their accessible names. It is a quick way to spot fields without labels, duplicated names (“Email address” twice) or names that include the error text by mistake. It does not show descriptions, so use it for names and Speech Viewer for everything else.
Frequently Asked Questions
Is NVDA good enough for testing form validation?
Yes. NVDA is free, widely used and closely follows the accessibility information browsers expose, which makes it a strong first choice for testing error announcements on Windows.
Why does NVDA not read my error when I tab away?
Because focus has moved to the next field. Errors linked with aria-describedby are read when the field regains focus; to announce on blur, also use a polite live region.
What does "invalid entry" mean in NVDA?
It is how NVDA announces aria-invalid="true" on a field. It should be followed by the error message from aria-describedby.
How do I get a text record of what NVDA said?
Open Speech Viewer from the NVDA Tools menu. It lists every phrase spoken, which you can copy into your test results.
Related Guides
- Screen Reader Testing for Forms — the overall method.
- Testing Form Errors with VoiceOver — the macOS and iOS equivalent.
- Building an Accessible Error Summary — what the summary should do.
- Linking Errors with aria-describedby — the markup behind the announcements.