Skip to content

Accessible Error Announcement

Standard HTML5 validation provides visual feedback (via :invalid or :user-invalid), but it doesn’t automatically synchronize with accessibility attributes like aria-invalid.

If you use standard :invalid styling, screen readers might announce “Invalid entry” the moment a user tabs into a required field that is currently empty. This creates a disruptive experience for users using assistive technologies, as the error is announced before interaction has occurred.

We want the programmatic state (aria-invalid="true") to be applied at the exact same moment the visual state (:user-invalid) applies. Since :user-invalid relies on the browser’s internal “user-interacted” flag, we can use JavaScript to check that this selector matches during standard interaction events.

Set aria-invalid="false" until :user-invalid matches, rather than leaving the attribute unset. Firefox and Safari expose an empty required field as invalid from page load, so an unset attribute still causes the premature “invalid entry” announcement there.

  1. Visual Layer: Use CSS :user-invalid to show borders/icons.
  2. Accessibility Layer: Use aria-invalid, and list the error message in aria-describedby while the field is invalid, to communicate state to Assistive Technology (AT). Announce an error that appears when the user leaves a field through one polite live region for the page.
  3. Bridge Visual & Accessibility Layer: Create a lightweight JavaScript utility that listens for blur, change, input and invalid events, checks if the element matches :user-invalid, and updates the ARIA attributes accordingly.
  4. Focus on Submit: Screen readers announce a field’s error when focus reaches the field, so after a failed submit, move focus to the first invalid field, or to an error summary (a heading followed by a link to each invalid field) on a long form. Native validation moves focus to the first invalid field for you; if the form uses novalidate, move it in your submit handler.

Link your input to its error message by setting the input’s data-error attribute to the error message’s id. The script in section 3 lists that id in aria-describedby only while the field is invalid. Don’t reference the error from aria-describedby in the markup: the error element is always in the page, and text that aria-describedby references is announced even when it’s hidden, so the error would be read out before the user has interacted.

<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
data-error="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>

Control the visibility of the error message using the native pseudo-class :user-invalid.

.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}

Since there is no “UserInvalidChanged” event, hook into standard form events to check the state.

Baseline: user-pseudos / aria-invalid

Baseline: user-pseudos

Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.

Open the demo standalone · Demo source on GitHub

Counter-example (negative demo) — what this guide tells you to avoid.


Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.