EthicsComputerCourses
Join training +91 90532 10052

Error Messages & Validation

Web Accessibility · Lesson 14 of 28 Intermediate 6 min read Updated

Everyone makes mistakes in forms: a missing digit in a mobile number, a date in the wrong format, a forgotten checkbox. What matters is how quickly the user can find and fix the mistake. For a screen reader user, a field that simply turns red gives no clue at all.

In this lesson you will learn what WCAG asks for in error handling, how to write helpful messages, how to connect them to fields in code, and how to build an error summary that works for keyboard and screen reader users.

What WCAG asks for

CriterionLevelIn short
3.3.1 Error IdentificationAIf an error is detected automatically, identify the field and describe the error in text
3.3.3 Error SuggestionAAIf you know how to fix it, tell the user (unless that would be a security risk)
3.3.4 Error Prevention (Legal, Financial, Data)AAFor payments, legal commitments and data changes, let users check, confirm or undo
1.4.1 Use of ColorAColour is not the only way of showing the error
4.1.3 Status MessagesAAMessages that appear without a focus change can be announced by assistive technology

Writing good error messages

A good error message is specific, polite and tells the user how to fix the problem:

WeakBetter
Invalid inputEnter your mobile number as 10 digits, for example 9876543210
Error in field 3Enter your date of birth
Wrong formatEnter the date as DD/MM/YYYY, for example 05/08/2004
RequiredSelect your exam centre

Put the message next to the field, start it with the word "Error" or an icon with text so it does not depend on colour, and keep the user's typed value so they only need to correct it.

Connecting errors to fields

Two attributes link an error to its field:

  • aria-invalid="true" marks the field as invalid, so screen readers announce "invalid entry".
  • aria-describedby points to the error message, so it is read when the field gets focus.

When the user submits a form with errors, show an error summary at the top: a heading, then a list of links to each problem field. Move focus to the summary so screen reader users hear it at once, and keyboard users can jump straight to each field.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Exam Registration</title>
  <style>
    body { font: 17px/1.5 system-ui, sans-serif; max-width: 480px; }
    label { display: block; font-weight: 600; margin-top: 14px; }
    input { font: inherit; padding: 6px; width: 100%; box-sizing: border-box; }
    .error { color: #b00020; display: block; }
    [aria-invalid="true"] { border: 3px solid #b00020; }
    #summary { border: 3px solid #b00020; padding: 8px 12px; }
    #summary:focus-visible { outline: 3px solid #0b5cad; }
  </style>
</head>
<body>
  <h1>Register for the mock test</h1>
  <div id="summary" tabindex="-1" hidden>
    <h2>There is a problem</h2>
    <ul id="summary-list"></ul>
  </div>

  <form id="reg" novalidate>
    <label for="name">Full name</label>
    <span class="error" id="name-error"></span>
    <input id="name" name="name" autocomplete="name" aria-describedby="name-error">

    <label for="mobile">Mobile number</label>
    <span class="error" id="mobile-error"></span>
    <input id="mobile" name="mobile" inputmode="numeric" autocomplete="tel-national" aria-describedby="mobile-error">

    <button type="submit" style="margin-top: 16px">Register</button>
  </form>

  <script>
    const form = document.querySelector("#reg");
    const rules = [
      { id: "name", test: (v) => v.trim() !== "", message: "Enter your full name" },
      { id: "mobile", test: (v) => /^[6-9]\d{9}$/.test(v), message: "Enter your mobile number as 10 digits, for example 9876543210" },
    ];
    form.addEventListener("submit", (event) => {
      event.preventDefault();
      const list = document.querySelector("#summary-list");
      list.replaceChildren();
      let errors = 0;
      for (const rule of rules) {
        const field = document.querySelector("#" + rule.id);
        const message = document.querySelector("#" + rule.id + "-error");
        if (rule.test(field.value)) {
          field.removeAttribute("aria-invalid");
          message.textContent = "";
        } else {
          errors++;
          field.setAttribute("aria-invalid", "true");
          message.textContent = "Error: " + rule.message;
          const link = document.createElement("a");
          link.href = "#" + rule.id;
          link.textContent = rule.message;
          const li = document.createElement("li");
          li.append(link);
          list.append(li);
        }
      }
      const summary = document.querySelector("#summary");
      summary.hidden = errors === 0;
      if (errors > 0) {
        document.title = "Error: Exam Registration";
        summary.focus();
      } else {
        document.title = "Exam Registration";
        form.replaceWith(Object.assign(document.createElement("p"), { textContent: "Registered. Check your SMS for the roll number." }));
      }
    });
  </script>
</body>
</html>

Press Register with empty fields. Focus moves to "There is a problem", the summary links jump to each field, and each field now has aria-invalid and a message read with its label. The page title also starts with "Error:", which screen readers often read first. Fill both fields correctly and submit again to see the success message.

The form uses novalidate to switch off the browser's own error bubbles so that the custom messages are used instead. The browser bubbles show one error at a time, disappear quickly, and cannot be styled or worded the way you want.

Native validation with :user-invalid

For simple forms, the browser's built-in validation is a fine starting point, and it is announced by most screen readers. Use the :user-invalid pseudo-class to show error styles only after the user has interacted with a field, instead of painting every empty field red the moment the page loads.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Native Validation</title>
  <style>
    input:user-invalid { border: 3px solid #b00020; }
    input:user-invalid + .msg::before { content: "Error: check this field. "; color: #b00020; }
  </style>
</head>
<body>
  <h1>Newsletter</h1>
  <form>
    <label for="mail">Email</label>
    <input id="mail" name="mail" type="email" required autocomplete="email">
    <span class="msg">For example, isha@example.in</span>
    <button>Subscribe</button>
  </form>
</body>
</html>

Type "isha" and move away: the field gets a red border and an "Error" prefix. Press Subscribe with the field empty and the browser shows its own message. Note that text added with CSS content may not be read by every screen reader, so for real forms, write messages as HTML text as in the first example.

Preventing costly mistakes

For actions that cost money, create legal commitments or delete data, 3.3.4 asks for at least one of these: the action can be undone, the data is checked and the user can fix errors, or the user can review and confirm before submitting.

HTML
<h2>Check your payment</h2>
<dl>
  <dt>Pay to</dt><dd>Jaipur Electricity Board, consumer 4410-2209</dd>
  <dt>Amount</dt><dd>₹1,299</dd>
</dl>
<a href="/pay/edit">Change these details</a>
<button type="submit">Confirm and pay ₹1,299</button>

Common mistakes

  • Only a red border. Colour alone fails 1.4.1; add text.
  • Messages far from the field, such as only at the top, or only in a toast that disappears.
  • Clearing the form after an error. Keep everything the user typed, except passwords if you must.
  • Validating on every keystroke. Messages that appear while someone is still typing are distracting and noisy for screen reader users. Validate on submit, or when the user leaves a field.
  • Vague messages such as "Invalid" or "Error 422".

Key points

  • Describe each error in text and identify the field (3.3.1); suggest the fix when you can (3.3.3).
  • Use aria-invalid="true" and link the message with aria-describedby.
  • On submit, show an error summary with links to fields and move focus to it.
  • Do not rely on colour alone; prefix messages with "Error" and keep the user's input.
  • For payments and legal or data changes, let users review, confirm or undo (3.3.4).

Exercise

Add a "PIN code" field to the Try-it registration form. It must be exactly 6 digits. Add a rule, an error message that explains the format, and make sure the summary includes it.

Show solution
HTML
<label for="pin">PIN code</label>
<span class="error" id="pin-error"></span>
<input id="pin" name="pin" inputmode="numeric" autocomplete="postal-code" aria-describedby="pin-error">
JavaScript
const rules = [
  { id: "name", test: (v) => v.trim() !== "", message: "Enter your full name" },
  { id: "mobile", test: (v) => /^[6-9]\d{9}$/.test(v), message: "Enter your mobile number as 10 digits, for example 9876543210" },
  { id: "pin", test: (v) => /^\d{6}$/.test(v), message: "Enter your PIN code as 6 digits, for example 124001" },
];

The new field follows the same pattern: a visible label, an empty error element linked with aria-describedby, and a rule whose message says exactly what format is expected. The loop adds it to the summary automatically.

Quick Quiz

1. Which attribute tells assistive technology that a field's value is wrong?

2. After a failed submit, where should focus go?

3. Which error message is best?

4. What does :user-invalid do?

Learn Web Accessibility with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call