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.

An NVDA test run Install NVDA, open Speech Viewer, load the form in Firefox or Chrome, work through the script in focus mode, check browse mode, and record the exact text from Speech Viewer. Install NVDA free, Windows Open Speech Viewer Tools menu Load the form Firefox or Chrome Run the script Tab, type, submit Record copy spoken text
Speech Viewer turns what NVDA says into text you can paste straight into a bug report.

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.

NVDA modes on a form A state diagram showing NVDA switching between browse mode and focus mode when the user tabs into a field, presses Escape, or toggles with NVDA+Space. Browse mode Focus mode Speech only Tab into field Escape live region update live region update
Tab into a field switches to focus mode; Escape returns to browse mode — note which mode each announcement happened in.

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.

What NVDA users hear Two columns contrasting announcements from a form with broken error wiring against a form with correct wiring. Broken wiring • "Email address, edit, required" • no mention of the error • "Continue, button" after submit • same alert on every keystroke Correct wiring ✓ "…invalid entry, required, Enter an email…" ✓ error read on return to field ✓ "There are 2 problems, heading" ✓ one announcement per blur or submit
The difference between the two columns is two attributes and a focus call.

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.

← Back to Testing & Accessibility