Skip to content

A11y 101 – 3.3.1 Error Identification

We’ve all been there. You fill out a form. You hit submit. Nothing happens. Maybe the border turns red. Maybe a little icon winks at you. Maybe your computer hums aggressively. But nobody tells you what went wrong.

You stare at the screen. “Did it fail? Did it work? Am I now married to a stranger?”

Silence is rude. Silence is inaccessible. And silence violates WCAG 3.3.1: Error Identification.

What to do

Don’t Ghost Me When I Mess Up

If an input error is automatically detected (like validation running on submit), the system must do two things:

  1. Identify the item in error. (Tell them which field broke.)
  2. Describe the error in text. (Tell them what broke.)

That means colors aren’t enough. Icons aren’t enough. Vague alerts like “Error occurred” aren’t enough. You need specific, text-based feedback tied directly to the field that messed up.

The Absurd Scenario

Usually, we write error messages that are helpful. “Password must contain a symbol.” “Email format invalid.” Boring, but necessary.

For this demo, let’s lean into the ridiculous. Let’s imagine a form where the backend hates you personally. Even with stupid errors, the mechanism of delivering them must be accessible. If you can handle the absurd with grace, you can handle the serious too.

The Bad Way (And Why It Hurts)

Here’s a snippet that looks fine visually but fails the criterion completely:

<!-- BAD: Silent Failure -->
<form id="badForm">
  <label for="username">Username</label>
  <input type="text" id="username"> <!-- Red border only, no text -->
  
  <label for="email">Email</label>
  <input type="email" id="email"> <!-- Red border only, no text -->
  
  <button type="submit">Submit</button>
</form>

<script>
  const form = document.getElementById('badForm');
  form.addEventListener('submit', function(e) {
    e.preventDefault();
    // Logic assumes fields are wrong but...
    // No message generated. Just a CSS class change.
    document.querySelectorAll('input').forEach(input => {
      input.style.borderColor = 'red'; 
    });
    
    // And maybe a toast notification somewhere unrelated
    alert("Form Failed!"); // Generic, not linked to input
  });
</script>

Why this fails:

  • Color blind users can’t see the red.
  • Screen reader users get nothing unless the alert catches focus.
  • There is no text describing why.
  • No connection between the field and the error.

The Good Way (With Ridiculous Errors)

Now let’s fix it. We’ll add aria-invalid to mark the state, and aria-describedby to point to the error text. I’ll write messages so terrible they hurt to read.

<!-- GOOD: Accessible Identification -->
<form id="goodForm">
  <label for="email-accessible">Your Email Address</label>
  <input type="email" id="email-accessible" aria-invalid="false">
  <span id="email-error" role="alert" style="display:none; color: #d32f2f;"></span>
  
  <br><br>
  
  <label for="age-accessible">Your Age</label>
  <input type="number" id="age-accessible" aria-invalid="false">
  <span id="age-error" role="alert" style="display:none; color: #d32f2f;"></span>
  
  <br><br>
  
  <button type="submit">Submit Anyway</button>
</form>

<script>
  const goodForm = document.getElementById('goodForm');
  
  goodForm.addEventListener('submit', function(e) {
    e.preventDefault();
    
    const emailInput = document.getElementById('email-accessible');
    const emailErr = document.getElementById('email-error');
    const ageInput = document.getElementById('age-accessible');
    const ageErr = document.getElementById('age-error');
    
    // Reset states
    emailInput.setAttribute('aria-invalid', 'false');
    emailErr.style.display = 'none';
    ageInput.setAttribute('aria-invalid', 'false');
    ageErr.style.display = 'none';
    
    let hasErrors = false;
    
    // Ridiculous Validation Logic
    const emailVal = emailInput.value;
    if (!emailVal.includes('@') || emailVal.length < 5) {
       // ERROR MESSAGE 1
       emailErr.textContent = "Your email is too shy to show an @ symbol.";
       emailInput.setAttribute('aria-invalid', 'true');
       emailErr.style.display = 'inline';
       
       // IMPORTANT: Link via ID
       emailInput.setAttribute('aria-describedby', 'email-error');
       hasErrors = true;
    }
    
    const ageVal = ageInput.value;
    if (!ageVal || ageVal > 150 || ageVal < 0) {
       // ERROR MESSAGE 2
       ageErr.textContent = "Math says you might be a time traveler or a ghost.";
       ageInput.setAttribute('aria-invalid', 'true');
       ageErr.style.display = 'inline';
       
       // IMPORTANT: Link via ID
       ageInput.setAttribute('aria-describedby', 'age-error');
       hasErrors = true;
    }
    
    if (hasErrors) {
      // Move focus to the FIRST error so user knows where to start
      document.activeElement.blur();
      emailInput.focus();
    }
  });
</script>

What works here:

  • Text Exists: “Too shy” and “Ghost” are weird, but they are text.
  • Identification: The specific inputs get flagged.
  • Programmatic Association: aria-describedby connects the message to the input. Screen readers announce the error when focused.
  • State: aria-invalid="true" signals the condition changes.
  • Focus Management: We move focus to the first error so the user isn’t lost.

Reality Check

I know I’m asking you to imagine ridiculous errors. In reality, you won’t likely use “too shy.” You’ll probably use “Invalid email.”

But the lesson is the same: 

Don’t hide the mistake.

Sometimes developers think being “polite” means being vague. “Something went wrong.” No. Being polite means being clear. If someone makes a mistake, tell them exactly where and exactly how to fix it.

Confusion breeds frustration.

Frustration breeds abandonment.

Abandonment breeds loss.

For users with disabilities, vagueness is an insurmountable wall. It extends infinitely in all directions. The surface is smooth without a notch or bump.

A clear message is just a locked door. With a clear handle to grab and the user is holding the key.

Common Pitfalls

  • Icons Only: A red exclamation mark next to the field. Great for sighted users. Invisible to everyone else.
    • Don’t forget the alternative text.
  • Inline Colors: Changing border color to red without text instructions nearby.
    • Changing border type from solid to dotted or dashed can help here.
  • Generic Alerts: Using alert() at the top of the page that doesn’t specify which field failed.
    • Resist using the JavaScript alert() as this will need to be dismissed. If the user has multiple errors they may forget. Place the errors in the page using the role="alert".
  • Missing Association: Putting error text below the field but not linking it with aria-describedby.
    • ARIA is starting to talk more about aria-notify and aria-errormessage. The adoption is happening, so I wouldn’t rely on this quite yet.

The Bottom Line

Mistakes happen. Your interface shouldn’t punish them twice by hiding the reason for the failure.

Give the text. Link the text. Point the finger gently but accurately.

And please, for the love of the web, don’t use “too shy.” Stick to professional wording. Leave the sass for the design team.

Let’s talk about it on LinkedIn and Bluesky!

Published ina11ya11y 101accessibilityADAblindcognitioncolorcontrastdevelopmentEAAEN 301 549W3CWCAGWeb Content Accessibility Guidelines

Comments are closed.