aria-busy and aria-invalid: Two Attributes Every Form Needs

30 March 2026 · 5 min read · Accessibility

We audited two forms at Onneta — our login page and our onboarding flow. Different pages, different purposes, different codebases. Both had the exact same accessibility gaps. That's not a coincidence. It's a pattern that lives in most web forms.

The Problem With Async Submit Buttons

When a user clicks "Sign in" or "Create account", the form makes an API call. During that call, the button text changes to "Signing in..." and gets disabled. Visually, this works. A sighted user sees the loading state.

But a screen reader user hears nothing. The button was announced as "Sign in" and now it's... silent. The page might be loading. It might have failed. There's no way to know without aria-busy.

The Fix: aria-busy on Async Buttons

The solution is one attribute and four lines of code:

// When submission starts
submitBtn.setAttribute('aria-busy', 'true');

// When submission ends (success or failure)
submitBtn.removeAttribute('aria-busy');

aria-busy="true" tells assistive technology that the element's content is being updated. Screen readers can announce "busy" or suppress intermediate announcements until the operation completes. When the attribute is removed, the final state is announced cleanly.

This needs to happen in every code path that ends the async operation — success, validation error, network failure, timeout. Miss one path and aria-busy stays stuck on, which is worse than not having it at all.

The Problem With Inline Validation

Our forms validate fields on submit and show error messages. The error text appears next to the field, connected via aria-describedby. Screen readers announce the error. Good so far.

But the input itself doesn't change state. The field has a red border (visual), an error message (text), and... no programmatic invalid state. A screen reader user tabbing through the form hears the field name but not that it failed validation.

The Fix: aria-invalid on Validated Fields

// When validation fails
emailInput.setAttribute('aria-invalid', 'true');

// When the user corrects the value
emailInput.removeAttribute('aria-invalid');

aria-invalid="true" marks the field as containing an error. Screen readers announce this alongside the field label, so users know immediately which fields need attention. Combined with aria-describedby pointing at the error message, this gives a complete picture: "Email, invalid, please enter a valid email address."

The Multi-Step Form Trap

Our onboarding flow has multiple steps. When a user clicks "Back" to return to a previous step, the old error messages and aria-invalid states from their earlier attempt were still visible. They'd see phantom errors from a form state that no longer existed.

The fix: every back-button handler must clear error state on the destination step:

function clearStepErrors(step) {
  const container = document.getElementById(step);
  // Clear visible error text
  container.querySelectorAll('.error-msg')
    .forEach(el => el.textContent = '');
  // Clear aria-invalid on all inputs
  container.querySelectorAll('[aria-invalid]')
    .forEach(el => el.removeAttribute('aria-invalid'));
}

This isn't optional. Any form with back navigation that doesn't clear error state will confuse both sighted users (stale red borders) and screen reader users (stale aria-invalid announcements).

Why This Keeps Happening

We found these identical issues in two independent forms. The reason is straightforward: most form tutorials don't teach ARIA states. They cover labels, placeholders, required fields. They don't cover what happens during and after async operations.

The result is forms that work perfectly for sighted mouse users and fail silently for everyone else. Not because developers don't care — because the gap isn't visible during manual testing.

The Checklist

After patching both forms, we now audit every form page with these checks:

Four checks. Takes two minutes with grep. Catches the same class of bug every time.

The Numbers

Across our login page and onboarding form, we applied 13 ARIA patches in two cycles. Every patch was a single-file change — no server restart, no deployment pipeline, no risk. The pattern repeats on every form: audit with grep, write the spec, execute the patch, verify with curl.

If you're building forms, run these four checks on yours. The gaps are almost certainly there. They've been there since the form was first written. And they're trivial to fix.

Accessible by default

Onneta audits every user-facing page for WCAG compliance. Your customers get an interface that works for everyone.

Start building with Onneta