All articles
Accessibility

Form Validation Accessibility: The Developer's Guide to ARIA and Error States

Stop relying on red text for form errors. Learn the exact ARIA choreography to build accessible forms, manage focus on submission, and correctly use aria-live regions for dynamic validation.

Kuda Zafevere·Full-Stack Engineer
·12 min read
Share:

A user submits a form. A red error appears somewhere on the page. But a screen reader user hears nothing useful, a keyboard-only user gets no guided path back to the broken field, and the team assumes the form is "good enough" because the message is visible. This is the silent failure behind a large share of broken onboarding, checkout, and account recovery flows.

Poor form validation accessibility is not a cosmetic issue. It creates abandonment, WCAG failures, and real product friction in exactly the flows that matter most. If users cannot perceive the error, understand what changed, or recover quickly, the form is still broken.

TL;DR: accessible form validation needs four things

  1. Required fields that are exposed programmatically.
  2. Invalid fields that are marked with aria-invalid when relevant.
  3. Error text that is explicitly associated with the field.
  4. Dynamic announcements or focus management so users know what changed after submit.

If you are already tightening accessibility checks in CI, pair this guide with A Practical Guide to Automated Accessibility Testing so you catch broken ARIA relationships and missing labels before manual QA starts.

Start with native HTML, then add ARIA where it helps

The first mistake teams make with accessible forms is treating ARIA like a replacement for HTML. It is not. Semantic inputs, real labels, correct input types, and the native required attribute come first. ARIA should clarify or supplement those semantics when needed, not paper over missing HTML.

For standard inputs, use the native control and the native required signal. Reach for aria-required mainly when you are working with a custom widget or a design system component that is not a plain HTML form control. A visual asterisk can help sighted users scan the form, but an asterisk alone is not enough because assistive technology does not infer required state from decoration.

Bad

<label htmlFor="email">Email *</label>
<input id="email" name="email" type="email" />

Why it fails: the asterisk is only visual. The control is not marked as required programmatically, so a user relying on assistive technology gets incomplete information before they ever submit the form.

Good

<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input
  id="email"
  name="email"
  type="email"
  required
  aria-describedby="email-note"
/>
<p id="email-note">Required. We will send your receipt here.</p>

Why it works: the field keeps native HTML semantics, the required state is exposed programmatically, and the visible supporting text also explains the expectation in plain language.

Native first, ARIA second

Use required on native inputs. Add aria-required for custom controls only when the native signal is unavailable or incomplete. ARIA supplements HTML; it does not replace good HTML.

Error linkage: how assistive tech knows which field is wrong

The most common implementation bug is simple: a team renders red text under an input and assumes proximity is enough. It is not. Assistive technology needs an explicit relationship between the field and the message.

In practice, field-level accessible error handling relies on a small set of attributes used deliberately rather than indiscriminately.

AttributeWhat it doesPractical rule
aria-invalidExposes that the current value is invalid.Toggle it on only when validation has actually failed.
aria-describedbyLinks the field to help text and supplemental instructions.Keep stable descriptive ids here, and include the error id too if your QA matrix benefits from that fallback.
aria-errormessagePoints specifically to the error message for an invalid field.Use it only while the field is invalid and the error is actually present.

Bad

<label htmlFor="email">Email</label>
<input id="email" />
<div className="text-red">Email is invalid</div>

Why it fails: the error is visible, but there is no programmatic link from the input to the message. A screen reader user may land on the field without hearing the actual reason it failed.

Good

<label htmlFor="email">Email</label>
<input
  id="email"
  aria-invalid="true"
  aria-describedby="email-help email-error"
  aria-errormessage="email-error"
/>
<p id="email-help">We will send your receipt here.</p>
<p id="email-error">Enter a valid email address.</p>

Why it works: the field exposes its invalid state, the help text remains associated, and the error message is tied directly to the input. That gives assistive technology a reliable way to announce both context and failure.

const hasEmailError = touched.email && !EMAIL_RE.test(values.email);

<input
  id="email"
  name="email"
  type="email"
  aria-invalid={hasEmailError || undefined}
  aria-describedby={hasEmailError ? "email-help email-error" : "email-help"}
  aria-errormessage={hasEmailError ? "email-error" : undefined}
/>

{hasEmailError ? (
  <p id="email-error">Enter a valid email address.</p>
) : null}

Toggle aria-invalid when the field is actually invalid, not on initial render. The same rule applies to aria-errormessage: set it only while the error is relevant. Many teams also keep the error id inside aria-describedby during the invalid state as a pragmatic compatibility layer rather than a dogmatic purity test.

Live regions: announcing errors that appear after interaction

aria-live exists for updates that happen after page load. If validation messages appear dynamically after blur, async validation, or submit, some users will need those updates announced because nothing in the DOM move guarantees they will discover the change.

Use aria-live="polite" for standard validation feedback. Reserve aria-live="assertive" or role="alert" for higher-priority submit failures or summary-level errors where interruption is justified. Too many interruptive announcements quickly become noisy and frustrating.

Bad

<div aria-live="assertive">{passwordError}</div>

Why it fails: if this updates on every keypress, the user is interrupted constantly. That is not guidance. It is audio spam.

Good

function LiveAnnouncer({ message }: { message: string }) {
  return (
    <div className="sr-only" aria-live="polite" aria-atomic="true">
      {message}
    </div>
  );
}

function SignupForm() {
  const [announcement, setAnnouncement] = useState("");
  const [errors, setErrors] = useState<Record<string, string>>({});

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const nextErrors = validateForm(new FormData(event.currentTarget));
    setErrors(nextErrors);

    const count = Object.keys(nextErrors).length;
    if (count > 0) {
      setAnnouncement(
        `${count} ${count === 1 ? "error" : "errors"} found. Review the highlighted fields.`
      );
    }
  }

  return (
    <>
      <LiveAnnouncer message={announcement} />
      <form noValidate onSubmit={handleSubmit}>{/* fields */}</form>
    </>
  );
}

This pattern announces a meaningful change once, at the moment it matters. It also keeps the live region reusable so your React accessible form errors stay consistent across flows.

Do not stack every technique at once

If you move focus to an error summary after submit, you often do not need a second assertive live region on top of it. Test the experience in a screen reader before adding more announcements.

Focus management on failed submit

After a failed submit, the user needs a recovery path. Two strategies are common and both can be accessible when implemented intentionally.

StrategyBest forKey implementation detail
Focus the error summaryLong forms, multi-step forms, or multiple simultaneous errors.Make the summary container focusable with tabIndex="-1" and provide links that move users directly to each field.
Focus the first invalid fieldShort forms or flows where immediate correction is faster than reading a summary.Focus the field directly and make sure the linked inline error is already exposed.

Strategy A works well when users need a map of what broke. Strategy B works well when there is only one or two fields and the fastest path is direct correction. The wrong move is not choosing either and leaving focus on the submit button with new errors scattered elsewhere in the page.

const summaryRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  if (!submitted || errors.length === 0) return;

  if (shouldFocusSummary) {
    summaryRef.current?.focus();
    return;
  }

  const firstField = document.getElementById(errors[0].fieldId) as HTMLElement | null;
  firstField?.focus();
}, [errors, submitted, shouldFocusSummary]);

function focusField(fieldId: string) {
  const field = document.getElementById(fieldId) as HTMLElement | null;
  if (!field) return;
  field.focus();
  field.scrollIntoView({ block: "center", behavior: "smooth" });
}

{errors.length > 0 && (
  <div ref={summaryRef} tabIndex={-1} aria-labelledby="form-errors-title">
    <h2 id="form-errors-title">Please fix the following errors</h2>
    <ul>
      {errors.map((error) => (
        <li key={error.fieldId}>
          <a
            href={`#${error.fieldId}`}
            onClick={(event) => {
              event.preventDefault();
              focusField(error.fieldId);
            }}
          >
            {error.label}: {error.message}
          </a>
        </li>
      ))}
    </ul>
  </div>
)}

For longer forms, focus the summary block and let users choose where to go next. For short forms, focus the first invalid field immediately. In both cases, summary links should move focus to the related control rather than only scrolling the page.

Native browser validation vs custom validation

Native HTML validation is not useless. In many browsers it will automatically focus the first invalid field, expose native required semantics, and prevent accidental submission. That baseline behavior is genuinely helpful.

The trade-off is control. Browser-native messages and UI are generic, browser-dependent, and harder to localize or style consistently. That is why many product teams use noValidate and implement custom validation logic instead.

If you choose custom validation, replicate the useful behavior intentionally: focus management, clear wording, linked error text, and dynamic announcements where needed. A stack like React Hook Form with Zod can make the state management cleaner, but the framework does not make the resulting markup accessible on its own.

Real-world context: validation on mobile and low-friction flows

Validation problems hurt most on mobile and in low-friction flows. Sign-up, onboarding, checkout, and fintech forms are usually time-sensitive and interruption-heavy. If a user has to guess which field failed, hunt for a message off-screen, or re-open the keyboard after every failed attempt, abandonment goes up quickly.

This matters in mixed-device environments as much as desktop-first ones. Whether a user is completing a form in Johannesburg, London, or anywhere else, the standard is the same: error states must be perceivable, operable, and understandable. Accessible validation is not a regional preference. It is a functional requirement.

Automated auditing vs manual testing

Automated tools are good at finding structural problems: missing labels, broken ARIA references, invalid attribute usage, and some contrast failures. They are not good at answering higher-level questions like "did focus land in the right place?" or "did this announcement actually help someone recover?"

That is why accessible form validation needs both automated auditing and manual validation flow testing. Use automation to reduce noise. Use human testing to validate the real interaction.

  • Run keyboard-only testing from the first field through final submit, with no mouse at all.
  • Do at least one screen reader pass using VoiceOver, NVDA, or another tool your team supports.
  • Confirm that error summary links move focus to the actual input, not just the viewport.
  • Check that live announcements fire when dynamic validation messages appear after user interaction.
  • Verify mobile behavior too, especially when the on-screen keyboard changes the viewport.

Use automation for structure, not final sign-off

Use the CodeAva Website Audit to scan for missing ARIA relationships, contrast issues, and structural form problems. Then validate the real flow with keyboard and screen reader testing, and use A Practical Guide to Automated Accessibility Testing to broaden the rest of your accessibility workflow.

Practical implementation checklist

  • Does every field have a real label?
  • Are required fields exposed programmatically?
  • Are invalid fields marked with aria-invalid only when invalid?
  • Is the error message linked to the field?
  • Are dynamic validation updates announced if needed?
  • On failed submit, does focus move somewhere intentional?
  • Can users jump directly from an error summary to the field?
  • Have you tested the form without a mouse?

Conclusion and next steps

Accessible validation is not just about showing an error in red. It is about state, relationships, focus, and announcements working together. If users cannot perceive the error or recover from it quickly, the form is still broken.

Start with strong native HTML, add ARIA only where it adds real signal, and test the finished flow the way users actually experience it. Use the CodeAva Website Audit to review structural accessibility issues, then read A Practical Guide to Automated Accessibility Testing for the broader testing workflow that keeps regressions out of production.

#accessible forms#form validation accessibility#aria-live#aria-invalid#aria-describedby#aria-errormessage#wcag#react accessibility

Frequently asked questions

More from Kuda Zafevere

Found this useful?

Share:

Want to audit your own project?

These articles are written by the same engineers who built CodeAva\u2019s audit engine.