Accessible Forms and Error Handling

โ–ถ Try It Yourself

Accessible Forms and Error Handling

1. Introduction

Forms are one of the most challenging areas of web accessibility. They require correct labelling, logical grouping, clear instructions, and informative error messages โ€” all communicated to sighted users, keyboard navigators, and screen reader users simultaneously. This lesson brings together everything you have learned about forms and accessibility into a production-ready, WCAG 2.1 Level AA conformant pattern.

2. Concept

Accessible Form Requirements

Requirement WCAG Criterion Implementation
Every control has a visible label 1.3.1 (A), 2.4.6 (AA) <label for> or aria-label
Required fields identified 3.3.2 (A) Visual indicator + aria-required=”true”
Instructions before fields 3.3.2 (A) aria-describedby pointing to hint text
Error messages identify the field 3.3.1 (A) Error text adjacent to field + role=”alert”
Suggestions provided after error 3.3.3 (AA) Specific correction guidance in error message
Focus moved to first error 2.4.3 (A) JavaScript .focus() on form submission
Note: Error messages must be associated programmatically with the invalid input using aria-describedby. A visual error message next to the field is insufficient โ€” the screen reader must announce the error when the user focuses the invalid field.
Tip: Use autocomplete attributes on all personal information fields. This is a WCAG 1.3.5 Level AA requirement and helps users with cognitive disabilities and motor impairments who struggle with repetitive data entry.
Warning: Never use placeholder as a substitute for a <label>. Placeholder text disappears when typing begins, fails contrast requirements in most browsers, and is not reliably announced as a field label by all screen readers.

3. Basic Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Accessible Form Demo</title>
    <style>
      .field { margin-bottom: 1.5rem; }
      label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
      .error { color: #cc0000; font-size: 0.875rem; margin-top: 0.25rem; }
      input[aria-invalid="true"] { border: 2px solid #cc0000; }
      :focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
    </style>
  </head>
  <body>
    <h1>Create Account</h1>

    <!-- Error summary โ€” shown on failed submission -->
    <div id="err-summary" role="alert" aria-live="assertive"
         aria-labelledby="err-title" tabindex="-1" hidden>
      <h2 id="err-title">There are errors in this form</h2>
      <ul id="err-list"></ul>
    </div>

    <form id="signup" novalidate>

      <div class="field">
        <label for="fname">
          Full Name <span aria-hidden="true">*</span>
        </label>
        <input type="text" id="fname" name="full_name"
          autocomplete="name" required
          aria-required="true"
          aria-describedby="fname-err"
          aria-invalid="false"
        >
        <p id="fname-err" class="error" role="alert" hidden>
          Full Name is required. Enter your first and last name.
        </p>
      </div>

      <div class="field">
        <label for="femail">
          Email <span aria-hidden="true">*</span>
        </label>
        <p id="femail-hint" style="font-size:.85rem;color:#555">We will send a confirmation to this address.</p>
        <input type="email" id="femail" name="email"
          autocomplete="email" required
          aria-required="true"
          aria-describedby="femail-hint femail-err"
          aria-invalid="false"
        >
        <p id="femail-err" class="error" role="alert" hidden>
          Enter a valid email address, e.g. name@example.com.
        </p>
      </div>

      <button type="submit">Create Account</button>
    </form>

    <script>
      document.getElementById('signup').addEventListener('submit', function(e) {
        e.preventDefault();
        var errors = [];
        var fname  = document.getElementById('fname');
        var femail = document.getElementById('femail');

        var fnErr = document.getElementById('fname-err');
        var fmErr = document.getElementById('femail-err');

        if (!fname.value.trim()) {
          fname.setAttribute('aria-invalid', 'true');
          fnErr.hidden = false;
          errors.push({id: 'fname', msg: 'Full Name: This field is required'});
        } else {
          fname.setAttribute('aria-invalid', 'false');
          fnErr.hidden = true;
        }

        if (!femail.value.trim() || !femail.validity.valid) {
          femail.setAttribute('aria-invalid', 'true');
          fmErr.hidden = false;
          errors.push({id: 'femail', msg: 'Email: Enter a valid email address'});
        } else {
          femail.setAttribute('aria-invalid', 'false');
          fmErr.hidden = true;
        }

        var summary = document.getElementById('err-summary');
        if (errors.length) {
          var list = document.getElementById('err-list');
          list.innerHTML = errors.map(function(er) {
            return '<li><a href="#' + er.id + '">' + er.msg + '</a></li>';
          }).join('');
          summary.hidden = false;
          summary.focus();
        } else {
          summary.hidden = true;
        }
      });
    </script>
  </body>
</html>

4. How It Works

Step 1 โ€” Required Field Indication

The asterisk (*) is wrapped in aria-hidden="true" to prevent screen readers announcing “asterisk” for every required field. The form includes a note that “fields marked * are required” for sighted users. aria-required="true" communicates the requirement to AT users directly.

Step 2 โ€” Error Summary Pattern

On submission failure, show an error summary with role="alert" and move focus to it. Each error links directly to the relevant field. This follows the GOV.UK design system pattern and is the recognised best practice for accessible error handling in large forms.

Step 3 โ€” Inline Errors with aria-invalid

Set aria-invalid="true" on the input and unhide the associated error paragraph when validation fails. Since aria-describedby already references the error element, when the user tabs back to the input the screen reader announces: label โ†’ “invalid” โ†’ error description.

Step 4 โ€” Multiple IDs in aria-describedby

aria-describedby="femail-hint femail-err" references both hint and error. The screen reader reads them in order: label โ†’ hint โ†’ error. When the error element is hidden, browsers skip it automatically โ€” no need to update aria-describedby dynamically.

5. Real-World Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Login</title>
    <style>
      body { font-family: system-ui, sans-serif; max-width: 400px; margin: 2rem auto; padding: 1rem; }
      .field { margin-bottom: 1.25rem; }
      label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
      input { width: 100%; padding: 0.5rem; font-size: 1rem; border: 1px solid #999; border-radius: 4px; box-sizing: border-box; }
      input[aria-invalid="true"] { border-color: #b91c1c; }
      .hint { font-size: 0.85rem; color: #555; margin-top: 0.2rem; }
      .error-msg { font-size: 0.85rem; color: #b91c1c; margin-top: 0.25rem; }
      :focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }
      .btn { width: 100%; padding: 0.6rem; font-size: 1rem; background: #2563eb; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
    </style>
  </head>
  <body>
    <main>
      <h1>Sign In</h1>

      <div role="alert" id="login-error" aria-live="assertive" hidden
           style="background:#fef2f2;border:1px solid #fca5a5;padding:.75rem;border-radius:4px;margin-bottom:1rem">
        <strong>Sign in failed.</strong> Your email or password is incorrect.
      </div>

      <form action="/login" method="post" novalidate>

        <div class="field">
          <label for="lemail">Email address</label>
          <input type="email" id="lemail" name="email"
            autocomplete="email" spellcheck="false"
            required aria-required="true"
            aria-invalid="false" aria-describedby="lemail-err"
          >
          <p id="lemail-err" class="error-msg" role="alert" hidden>Enter your email address.</p>
        </div>

        <div class="field">
          <label for="lpass">Password</label>
          <p id="pass-hint" class="hint">Your password is case-sensitive.</p>
          <input type="password" id="lpass" name="password"
            autocomplete="current-password"
            required aria-required="true"
            aria-invalid="false" aria-describedby="pass-hint lpass-err"
          >
          <p id="lpass-err" class="error-msg" role="alert" hidden>Enter your password.</p>
        </div>

        <div class="field">
          <label>
            <input type="checkbox" name="remember" value="1">
            Keep me signed in for 30 days
          </label>
        </div>

        <button type="submit" class="btn">Sign In</button>
        <p style="margin-top:1rem"><a href="/forgot-password">Forgot your password?</a></p>
      </form>
    </main>
  </body>
</html>

6. Common Mistakes

Error message not linked to the field

<input type="email" id="email">
<p style="color:red">Invalid email</p>

Use aria-describedby and role=”alert” to link error to field

<input type="email" id="email" aria-describedby="email-err" aria-invalid="true">
<p id="email-err" role="alert">Enter a valid email, e.g. name@example.com</p>

Placeholder as the only label

<input type="text" placeholder="Your full name">

Always provide a persistent visible label above each input

<label for="fname">Full name</label>
<input type="text" id="fname" placeholder="e.g. Jane Smith">

7. Try It Yourself

▶ Try It Yourself

8. Quick Reference

Pattern Attributes Used WCAG Criterion
Visible label <label for> 1.3.1, 2.4.6 (A/AA)
Required indicator aria-required=”true” 3.3.2 (A)
Hint text aria-describedby 3.3.2 (A)
Inline error aria-invalid=”true” + role=”alert” 3.3.1 (A)
Error summary role=”alert” + focus management 3.3.3 (AA)
Autocomplete autocomplete=”name” etc. 1.3.5 (AA)
Correct input type type=”email”, type=”tel” etc. 1.3.5 (AA)

9. Quiz

🧠 Test Yourself

Which two attributes must you set on an invalid input so screen readers announce the error when the field is focused?





โ–ถ Try It Yourself