How to Build Accessible Form Errors in React

Build accessible React form errors with visible text, aria-invalid, aria-describedby, and a focused error summary that screen readers announce.

6 min read

Accessible form errors reach every user, not just those who can see a red border. They combine visible text, a programmatic link from each field to its message, and an announcement for screen readers. React adds the pieces with aria-invalid, aria-describedby, and role alert.

Most of the work is HTML and ARIA, not React, so the same patterns work with any state approach, from plain useState to a form library.

Show text, not just color

Color alone is not an error message. A user with colorblindness or a screen reader cannot rely on a red border, so render the message as text and tie the styling to the aria-invalid state.

App.jsxApp.jsx
<input
  id="email"
  type="email"
  value={email}
  aria-invalid={error ? "true" : "false"}
  aria-describedby="email-error"
  onChange={(e) => setEmail(e.target.value)}
/>
{error && <p id="email-error">{error}</p>}

aria-invalid flags the field for assistive technology, and styling the selector [aria-invalid="true"] keeps the visual and the programmatic state in one place. The message appears as real text under the input.

Use native HTML attributes first

Before adding ARIA, use the browser's built-in checks. type=email, required, minLength, and pattern validate without any JavaScript, and the browser focuses the first invalid field for you. Their error messages are inconsistent across browsers, which is why custom text plus aria-invalid is the upgrade, not the replacement.

The noValidate prop turns off those native tooltips when you render your own messages, so the two systems do not fight over the same field.

Connect the message to the field

aria-describedby links the field to its message, so a screen reader reads the error when focus returns to the input. The message needs an id that matches the attribute. An aria-errormessage attribute points at the same id when you want to mark it specifically as an error, but aria-describedby is the wider-supported choice.

App.jsxApp.jsx
{error && <p id="email-error" role="alert">{error}</p>}

The role alert makes the message announce as soon as it appears, even if focus is elsewhere. Keep the id unique per field, because two inputs cannot share the same describedby target.

Announce form-level errors

A failure that is not tied to one field, such as a failed request or a duplicate email, belongs at the top of the form with a live region.

App.jsxApp.jsx
{errors.form && (
  <div role="alert">
    <p>Fix these errors to continue.</p>
    <ul>
      <li>{errors.form}</li>
    </ul>
  </div>
)}

The alert region announces the message when it mounts. Use it once per form for the summary, and let individual fields keep their own smaller messages below them. Multiple live regions compete for the screen reader's attention, so keep announcements to one clear message.

Focus an error summary after a failed submit

When submit fails, move keyboard focus to the summary so the user starts from the error instead of being left on a disabled button.

App.jsxApp.jsx
function handleSubmit(e) {
  e.preventDefault();
  const nextErrors = validate(values);
  setErrors(nextErrors);
  if (Object.keys(nextErrors).length > 0) {
    summaryRef.current.focus();
  }
}

The summary needs a tabIndex of -1 so it can receive focus programmatically, not 0, so it is not an extra tab stop during normal navigation. Focus lands on the summary, the alert announces the problem, and the user can tab into the first invalid field to fix it.

Validate at the right moment

Do not mark empty required fields invalid while the user is still typing. Run validation on submit first, then recheck a field on blur only after it has already failed. Marking a field invalid too early interrupts a user who has not finished typing, and it trains screen reader users to ignore the warning.

  • Validate on submit, then recheck failed fields on blur.
  • Leave clean fields alone until the user attempts to submit.
  • Clear a field's error as soon as its value becomes valid.

For the validation logic itself, see React form validation without a library. For errors that come back from a server, see how to show server validation errors in React forms, and for the surrounding form, see how to build forms in React.

Rune AI

Rune AI

Key Insights

  • Show error text, never color alone.
  • Mark invalid fields with aria-invalid.
  • Link each message with aria-describedby.
  • Announce form errors with role=alert.
  • Focus an error summary after a failed submit.
RunePowered by Rune AI

Frequently Asked Questions

Should I show errors only with color?

No. Color is invisible to colorblind and screen reader users. Always show text, and style invalid fields with the aria-invalid attribute selector so the meaning is not tied to color.

How do I connect an error message to its field?

Give the message an id and point the field at it with aria-describedby. Screen readers then read the message when the field regains focus.

Should I mark empty required fields invalid on load?

No. Wait until the user submits or leaves the field. Marking fields invalid too early interrupts users who are still filling out the form.

Conclusion

Accessible form errors need visible text, a programmatic connection to the field, and an announcement. Mark fields with aria-invalid, link messages with aria-describedby, and focus a summary on failed submit.