Testing Form Errors with VoiceOver

VoiceOver is built into every Mac, iPhone and iPad, so it costs nothing to test with and it is what a large share of screen reader users on Apple devices rely on. It also behaves differently from Windows screen readers in ways that matter for form validation: it has no separate browse and focus modes, it reads descriptions after a short pause as “hints”, it announces invalid fields with different wording, and on iOS it is driven by touch gestures rather than the keyboard. This walkthrough covers setting up VoiceOver for testing on macOS with Safari, the caption panel that shows what was spoken, a concrete pass through a form with errors, and the same test on iOS. It applies the method in screen reader testing for forms and pairs with testing form errors with NVDA.

The failure this guide prevents: assuming that because NVDA reads your errors correctly, VoiceOver users hear them too.

A VoiceOver test run on macOS Turn on VoiceOver, enable the caption panel, open the form in Safari, run the script with Tab and VoiceOver keys, and record the captions for each step. Cmd+F5 start VoiceOver Caption panel on by default Safari load the form Run the script Tab, VO keys, rotor Record copy caption text
Safari is the browser VoiceOver is designed around; test there first.

Prerequisites

Requirement Notes
A Mac with a current macOS VoiceOver is built in
Safari The best-supported browser for VoiceOver
An iPhone or iPad For the touch test
The form’s test script Same steps as for other screen readers
Keyboard navigation enabled See Step 1

Step 1: Set Up macOS for Testing

Turn VoiceOver on and off with Cmd+F5. The VO keys are Ctrl+Option by default; commands below write them as VO. The caption panel at the bottom of the screen shows what VoiceOver says; keep it visible, and use VO+Z to repeat the last phrase if you missed it.

Two settings affect form testing. First, enable keyboard navigation so Tab reaches every control: in System Settings → Keyboard, turn on Keyboard navigation, and in Safari’s Advanced settings enable Press Tab to highlight each item on a webpage. Without these, Tab skips links and buttons, including your error summary links. Second, leave VoiceOver’s verbosity at its defaults; hints (which is how descriptions are read) are on by default, and that is what users hear.

Step 2: Navigate the Way VoiceOver Users Do

VoiceOver has no browse or focus mode switch. Users move with Tab, with VO+Right/Left arrow through every element, or by jumping between controls with VO+Cmd+J (next form control). The rotor, opened with VO+U, lists headings, links and form controls; use the arrow keys to switch lists. Test all three styles at least once, because errors reachable by Tab are not always reachable by VO arrows, and vice versa.

VoiceOver reading an invalid field When focus reaches an invalid field, VoiceOver reads the label, the invalid state and the control type, pauses, then reads the description from aria-describedby as a hint. Tester Safari VoiceOver Shift+Tab to email name, invalid, description Email address, invalid data, edit text short pause hint with the error message
VoiceOver reads descriptions after a short pause — wait for the hint before deciding the error was not announced.

Step 3: Walk Through the Form

Use the same form as in the NVDA guide: email (required, type="email"), postcode (required), Continue button, and an error summary that receives focus on a failed submit. The announcements below are typical for VoiceOver with Safari on a recent macOS; exact wording changes between releases.

Tab to the email field. Expect the label, “required”, the control type and, after a pause, the hint: “Email address, required, edit text”“We’ll send your receipt here.” If the hint never comes, check aria-describedby and that hints are enabled.

Type an invalid value and Tab away. As with NVDA, a description-only error is not heard until you return. A polite live region speaks after the next field’s announcement.

Return with Shift+Tab. Expect: “Email address, invalid data, required, edit text”“Enter an email address like name@example.com.” “Invalid data” comes from aria-invalid="true". Wait for the pause before concluding the message is missing; this is the single most common false failure in VoiceOver testing.

Leave postcode empty and activate Continue with VO+Space or Enter. Expect the summary heading: “There are 2 problems, heading level 2.” Then Tab or VO+Right to the links. If VoiceOver says nothing or re-reads the button, focus did not move.

Follow the first link. Expect the email field with its error hint. If VoiceOver’s cursor moves but keyboard focus does not, typing will go elsewhere — a sign that the link target is not the input itself.

Fix and submit. Expect a success message or new page title to be announced.

Step 4: Repeat on iOS

On an iPhone, turn VoiceOver on in Settings → Accessibility → VoiceOver, or set the Accessibility Shortcut so a triple-click of the side button toggles it. The gestures you need:

Action Gesture
Next / previous element Swipe right / left with one finger
Activate Double-tap
Rotor Rotate two fingers on the screen
Next item of rotor type Swipe down with one finger
Read from here Swipe down with two fingers

Run the script by swiping. Listen for errors placed before their field in the DOM, which are read before the label and feel disconnected, and for fixed-position banners the swipe order never reaches. Using the rotor set to Form Controls jumps between fields — check that each invalid field announces its error there too. On-screen keyboard behaviour matters: after a failed submit, iOS may keep the keyboard open over the summary, so check that focus and the reading position both reach it.

VoiceOver compared with NVDA Two columns listing how VoiceOver and NVDA differ when announcing form errors. VoiceOver • no browse / focus mode • "invalid data" • description read as a delayed hint • touch gestures on iOS NVDA • browse and focus modes • "invalid entry" • description read straight after state • keyboard only
The same markup produces different announcements; test both instead of assuming one covers the other.

The Markup VoiceOver Needs

The markup is the same as for any screen reader: a real <label>, aria-invalid set only after interaction, and an error element referenced by aria-describedby, kept in the DOM with its text changed rather than created on demand.

<form novalidate>
  <label for="email">Email address</label>
  <input id="email" name="email" type="email" required
         aria-describedby="email-hint email-error">
  <p id="email-hint">We'll send your receipt here.</p>
  <p id="email-error" hidden></p>
</form>
function focusSummary(summary: HTMLElement): void {
  summary.hidden = false;
  const heading = summary.querySelector<HTMLElement>("h2")!;
  heading.tabIndex = -1;
  // Safari/VoiceOver sometimes misses focus on content that was just un-hidden
  requestAnimationFrame(() => heading.focus());
}

Deferring the focus call by one frame after un-hiding the summary gives Safari time to update its accessibility tree; without it, VoiceOver occasionally keeps reading from the old position. Test with and without the delay on your own form, since the behaviour varies between releases.

Common Failures and Their Fixes

Error seems not to be read. Wait for the hint pause, or press VO+Z to repeat the last phrase. If still silent, check aria-describedby.

Summary links do nothing. Tab did not reach them because keyboard navigation is off in Safari, or the links point to ids that do not exist. Enable Tab highlighting and verify the targets.

Hidden error text read aloud. An empty error element hidden with CSS opacity: 0 or off-screen positioning is still in the accessibility tree. Use hidden or display: none when there is no error.

Live region silent on first update. VoiceOver can miss updates to a live region that was added to the DOM at the same moment. Render the live region empty on page load and change only its text later, the approach tested in asserting aria-live announcements in Playwright.

VoiceOver with Chrome

Many Mac users browse with Chrome rather than Safari, and VoiceOver works with it, but not identically. Chrome exposes the same ARIA attributes, yet the timing of announcements after focus moves, the handling of live regions that change quickly, and the reading of descriptions can all differ from Safari. If your analytics show a significant Chrome share on macOS, run the script once in Chrome per release cycle and record the browser with each result. When a step passes in Safari and fails in Chrome (or the reverse), the cause is usually timing — focus moved before content rendered, or a live region updated twice in quick succession — rather than missing markup, so start by checking the order of DOM updates and focus calls.

Recording Results

Copy the caption panel’s text for each step into the same results table used for NVDA, with the macOS or iOS version and Safari version. Keeping both screen readers’ results side by side makes differences obvious: a step that passes in NVDA and fails in VoiceOver usually points to timing, hidden content or a Safari focus quirk rather than missing markup, which narrows the fix.

Frequently Asked Questions

Which browser should I use with VoiceOver?

Safari. VoiceOver is designed around Safari on macOS and iOS, and most VoiceOver users browse with it. Test Chrome as well if your analytics show many Mac users on it.

Why does VoiceOver not read my error message straight away?

VoiceOver reads aria-describedby text as a hint after a short pause. Wait for it, or press VO+Z to repeat the last phrase.

What does "invalid data" mean in VoiceOver?

It is how VoiceOver announces aria-invalid="true". It should be followed, after the pause, by the error message linked with aria-describedby.

Do I need to test on iOS as well as macOS?

Yes, for important forms. iOS users navigate by swiping, which changes reading order and exposes errors placed before their fields or in fixed banners.

← Back to Testing & Accessibility