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.
<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.
{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.
{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.
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
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.
Frequently Asked Questions
Should I show errors only with color?
How do I connect an error message to its field?
Should I mark empty required fields invalid on load?
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.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.