Blog

Form Accessibility Guide: Labels, Errors, and WCAG Compliance

TestParty
TestParty
July 22, 2025

Form accessibility failures block users with disabilities from completing critical tasks—account creation, checkout, contact submissions, and any data entry interaction. Missing form labels are the third most common accessibility error, appearing on 45% of home pages according to the WebAIM Million. For e-commerce sites, inaccessible forms mean lost sales and legal liability.

WCAG requires form inputs to have associated labels (1.3.1), instructions to be provided (3.3.2), errors to be identified (3.3.1), and suggestions to be offered (3.3.3). Meeting these requirements ensures all users can complete forms successfully.

Q: What makes a form WCAG compliant?

A: WCAG-compliant forms have programmatically associated labels for every input, clear instructions where needed, error messages that identify the problem and suggest fixes, and accessible validation that doesn't rely solely on color. Users must be able to complete the form using keyboard alone.

Form Labels Fundamentals

Why Labels Matter

Screen readers announce form field labels. Without them, users hear "edit text" with no context—they don't know what information to enter.

Screen reader experience without label:

"Edit text" (User has no idea what to enter)

-

Screen reader experience with label:

"Email address, edit text" (User knows exactly what's needed)

-

Proper Label Association

Explicit association with `for` attribute (preferred):

<label for="email">Email address</label>
<input type="email" id="email" name="email">

Implicit association by wrapping:

<label>
  Email address
  <input type="email" name="email">
</label>

Using `aria-labelledby`:

<span id="email-label">Email address</span>
<input type="email" aria-labelledby="email-label">

Using `aria-label` (for visually hidden labels):

<input type="search" aria-label="Search products">

Label Best Practices

Always visible: Labels should be visible, not just placeholders. Placeholders disappear when users type, leaving no context.

Above or beside input: Position labels consistently—typically above or to the left of inputs.

Concise but clear: "Email" is better than "Please enter your email address here."

Match field purpose: The label should accurately describe what input is expected.

Placeholder vs Label

Placeholders are NOT labels:

<!-- Wrong: Placeholder alone doesn't provide accessible label -->
<input type="email" placeholder="Email address">

<!-- Right: Label plus optional placeholder -->
<label for="email">Email address</label>
<input type="email" id="email" placeholder="john@example.com">

Placeholder problems:

  • Disappears when typing
  • Often low contrast (fails 4.5:1 requirement)
  • Not announced consistently by screen readers
  • Users may mistake placeholder for entered value

Required Fields

Indicating Required Status

Visual indication:

  • Asterisk (*) with explanation
  • "Required" text
  • Or indicate optional fields instead

Programmatic indication:

<!-- Using required attribute -->
<label for="email">Email address *</label>
<input type="email" id="email" required>

<!-- Using aria-required for custom validation -->
<label for="email">Email address *</label>
<input type="email" id="email" aria-required="true">

Legend for asterisks:

<p class="required-legend">* Required fields</p>

<label for="name">Full name *</label>
<input type="text" id="name" required>

Screen Reader Announcements

When required attribute is present, screen readers announce:

"Email address, required, edit text"

-

This tells users they must complete the field before submitting.

Error Handling

WCAG Error Requirements

3.3.1 Error Identification (Level A): If an error is detected, the item in error must be identified and described in text.

3.3.3 Error Suggestion (Level AA): If an error is detected and suggestions are known, provide them (unless it would compromise security).

Error Message Best Practices

Be specific: "Please enter a valid email address" not "Invalid input"

Be helpful: Explain what's wrong AND how to fix it

Associate with field: Error messages must be programmatically linked to their fields

<!-- Error message associated with field -->
<label for="email">Email address</label>
<input type="email"
       id="email"
       aria-describedby="email-error"
       aria-invalid="true">
<span id="email-error" class="error">
  Please enter a valid email address (example: name@domain.com)
</span>

Announcing Errors to Screen Readers

Option 1: Live region for error summary

<div role="alert" aria-live="assertive" id="error-summary">
  <!-- Populated when errors occur -->
</div>

<script>
function showErrors(errors) {
  const summary = document.getElementById('error-summary');
  summary.textContent = `${errors.length} errors found. Please correct the highlighted fields.`;
}
</script>

Option 2: Focus management on submit

function handleSubmit(event) {
  const errors = validateForm();
  if (errors.length > 0) {
    event.preventDefault();
    // Move focus to first error
    const firstErrorField = document.getElementById(errors[0].fieldId);
    firstErrorField.focus();
    // Error message already associated via aria-describedby
  }
}

Error Display Patterns

Inline errors (recommended):

<div class="form-field error">
  <label for="password">Password</label>
  <input type="password"
         id="password"
         aria-invalid="true"
         aria-describedby="password-error">
  <span id="password-error" class="error-message">
    Password must be at least 8 characters
  </span>
</div>

Error summary plus inline:

<div role="alert" class="error-summary">
  <h2>Please fix the following errors:</h2>
  <ul>
    <li><a href="#email">Email address is required</a></li>
    <li><a href="#password">Password is too short</a></li>
  </ul>
</div>

<!-- Individual fields have inline errors too -->

Don't Rely on Color Alone

Wrong: Only red border indicates error

.error input {
  border-color: red; /* Color-blind users may miss this */
}

Right: Multiple indicators

<div class="form-field error">
  <label for="email">Email address</label>
  <input type="email"
         id="email"
         aria-invalid="true"
         aria-describedby="email-error">
  <!-- Icon + text error message provides non-color indication -->
  <span id="email-error" class="error-message">
    âš  Please enter a valid email address
  </span>
</div>

Form Instructions

When Instructions Are Needed

Provide instructions when:

  • Format is required (dates, phone numbers)
  • Specific constraints apply (character limits, allowed characters)
  • Field purpose isn't obvious from label alone

Implementing Instructions

Using `aria-describedby`:

<label for="phone">Phone number</label>
<input type="tel"
       id="phone"
       aria-describedby="phone-instructions">
<span id="phone-instructions" class="instructions">
  Format: (555) 555-5555
</span>

In-label instructions:

<label for="username">
  Username
  <span class="instructions">(letters and numbers only)</span>
</label>
<input type="text" id="username">

Character counters:

<label for="bio">Bio</label>
<textarea id="bio"
          aria-describedby="bio-count"
          maxlength="200"></textarea>
<span id="bio-count" aria-live="polite">
  <span class="count">0</span>/200 characters
</span>

Input Types and Autocomplete

Appropriate Input Types

Using correct input types enables:

  • Appropriate mobile keyboards
  • Built-in browser validation
  • Autocomplete functionality
<input type="email">      <!-- Email keyboard, @ button -->
<input type="tel">        <!-- Phone keypad -->
<input type="number">     <!-- Numeric keypad -->
<input type="url">        <!-- URL keyboard -->
<input type="date">       <!-- Date picker -->
<input type="search">     <!-- Search functionality -->

Autocomplete Attributes

WCAG 1.3.5 (Level AA) requires autocomplete for user data:

<input type="text" autocomplete="name">
<input type="email" autocomplete="email">
<input type="tel" autocomplete="tel">
<input type="text" autocomplete="street-address">
<input type="text" autocomplete="postal-code">
<input type="text" autocomplete="cc-number"> <!-- Credit card -->
<input type="text" autocomplete="cc-exp">    <!-- Expiration -->

Benefits:

  • Users can autofill from saved data
  • Reduces typing for motor-impaired users
  • Improves checkout conversion
  • Required for WCAG 2.1 AA compliance

Complex Form Patterns

Fieldsets and Legends

Group related fields using <fieldset> and <legend>:

<fieldset>
  <legend>Shipping Address</legend>

  <label for="ship-street">Street address</label>
  <input type="text" id="ship-street" autocomplete="shipping street-address">

  <label for="ship-city">City</label>
  <input type="text" id="ship-city" autocomplete="shipping address-level2">

  <label for="ship-zip">ZIP code</label>
  <input type="text" id="ship-zip" autocomplete="shipping postal-code">
</fieldset>

<fieldset>
  <legend>Billing Address</legend>
  <!-- Similar fields with billing autocomplete -->
</fieldset>

Radio Button Groups

<fieldset>
  <legend>Shipping speed</legend>

  <input type="radio" id="standard" name="shipping" value="standard">
  <label for="standard">Standard (5-7 days) - Free</label>

  <input type="radio" id="express" name="shipping" value="express">
  <label for="express">Express (2-3 days) - $9.99</label>

  <input type="radio" id="overnight" name="shipping" value="overnight">
  <label for="overnight">Overnight - $24.99</label>
</fieldset>

Checkbox Groups

<fieldset>
  <legend>Email preferences</legend>

  <input type="checkbox" id="news" name="prefs" value="news">
  <label for="news">Newsletter</label>

  <input type="checkbox" id="promos" name="prefs" value="promos">
  <label for="promos">Promotions and sales</label>

  <input type="checkbox" id="updates" name="prefs" value="updates">
  <label for="updates">Order updates</label>
</fieldset>

Multi-Step Forms

For forms spanning multiple pages:

<!-- Progress indicator -->
<nav aria-label="Form progress">
  <ol>
    <li aria-current="step">Step 1: Shipping</li>
    <li>Step 2: Payment</li>
    <li>Step 3: Review</li>
  </ol>
</nav>

<!-- Step content -->
<main>
  <h1>Shipping Information</h1>
  <!-- Form fields -->
</main>

<!-- Navigation -->
<button type="button">Back</button>
<button type="submit">Continue to Payment</button>

E-Commerce Form Patterns

Checkout Forms

<form>
  <fieldset>
    <legend>Contact Information</legend>

    <label for="email">Email address</label>
    <input type="email" id="email" autocomplete="email" required>

    <label for="phone">Phone number</label>
    <input type="tel" id="phone" autocomplete="tel">
  </fieldset>

  <fieldset>
    <legend>Shipping Address</legend>

    <label for="name">Full name</label>
    <input type="text" id="name" autocomplete="name" required>

    <label for="address">Street address</label>
    <input type="text" id="address"
           autocomplete="street-address" required>

    <label for="city">City</label>
    <input type="text" id="city"
           autocomplete="address-level2" required>

    <label for="state">State</label>
    <select id="state" autocomplete="address-level1" required>
      <option value="">Select state</option>
      <!-- State options -->
    </select>

    <label for="zip">ZIP code</label>
    <input type="text" id="zip"
           autocomplete="postal-code"
           required
           pattern="[0-9]{5}(-[0-9]{4})?"
           aria-describedby="zip-format">
    <span id="zip-format" class="hint">5 or 9 digit ZIP</span>
  </fieldset>

  <button type="submit">Continue to Payment</button>
</form>

Payment Forms

<fieldset>
  <legend>Payment Information</legend>

  <label for="cc-name">Name on card</label>
  <input type="text" id="cc-name" autocomplete="cc-name" required>

  <label for="cc-number">Card number</label>
  <input type="text" id="cc-number"
         autocomplete="cc-number"
         required
         inputmode="numeric"
         pattern="[0-9\s]{13,19}">

  <div class="inline-fields">
    <div>
      <label for="cc-exp">Expiration (MM/YY)</label>
      <input type="text" id="cc-exp"
             autocomplete="cc-exp"
             required
             placeholder="MM/YY"
             pattern="(0[1-9]|1[0-2])\/[0-9]{2}">
    </div>
    <div>
      <label for="cc-csc">Security code</label>
      <input type="text" id="cc-csc"
             autocomplete="cc-csc"
             required
             inputmode="numeric"
             maxlength="4"
             aria-describedby="csc-help">
      <span id="csc-help">3 or 4 digits on card</span>
    </div>
  </div>
</fieldset>

Testing Form Accessibility

Automated Testing

TestParty identifies:

  • Missing form labels
  • Empty or duplicate labels
  • Missing required field indicators
  • Forms lacking error handling patterns
  • Missing autocomplete attributes

Manual Testing Checklist

Keyboard testing:

  • [ ] Can tab through all fields in logical order
  • [ ] Can interact with all controls (selects, checkboxes, radios)
  • [ ] Submit button is keyboard accessible
  • [ ] No keyboard traps in form

Screen reader testing:

  • [ ] All fields have announced labels
  • [ ] Required status is announced
  • [ ] Instructions are announced with fields
  • [ ] Error messages are announced
  • [ ] Form purpose is clear from announcements

Visual testing:

  • [ ] Labels are visible (not placeholder-only)
  • [ ] Required fields are indicated
  • [ ] Errors use more than color
  • [ ] Instructions are visible when needed

FAQ Section

Q: Can I use placeholder instead of label for cleaner design?

A: No. Placeholders are not accessible replacements for labels—they disappear when typing, have contrast issues, and aren't announced consistently. Use visible labels; supplement with placeholder if desired.

Q: How do I handle validation—inline or on submit?

A: Both patterns can be accessible. Inline validation provides immediate feedback but can be annoying for screen reader users if too aggressive. On-submit validation is simpler but requires good error summary. Consider gentle inline validation (validate on blur, not keystroke) combined with submit-time summary.

Q: Should I use HTML5 validation or custom?

A: HTML5 validation (required, pattern, type) provides baseline accessibility. Custom validation allows better error messages and consistent styling. Often the best approach is HTML5 attributes for programmatic state plus custom messages for user experience.

Q: How do I make CAPTCHA accessible?

A: CAPTCHAs are inherently problematic for accessibility. Provide audio alternatives. Consider alternatives: honeypot fields, time-based validation, or reCAPTCHA with accessible fallbacks. WCAG 2.2's Accessible Authentication criteria address this.

Q: What about date picker accessibility?

A: Custom date pickers need careful keyboard handling and ARIA. Native <input type="date"> has improved significantly but varies by browser. Always allow direct text entry as alternative to picker interface.

Key Takeaways

  • Every form field needs a programmatic label. No exceptions. Placeholder text isn't a label.
  • Required fields must be indicated visually and programmatically (using required or aria-required).
  • Error messages must identify the problem and suggest how to fix it. Associate messages with fields using aria-describedby.
  • Don't rely on color alone for any status indication. Use text, icons, and other non-color indicators.
  • Use appropriate input types and autocomplete attributes for better user experience and WCAG compliance.
  • Group related fields with <fieldset> and <legend> for context.

Conclusion

Form accessibility determines whether users with disabilities can complete critical tasks on your site. For e-commerce, inaccessible checkout forms directly translate to lost sales and legal exposure.

The fundamentals are straightforward: label every field, indicate required status, provide clear error messages, don't rely on color alone. Getting these basics right covers the majority of form accessibility requirements.

TestParty identifies form accessibility issues and provides specific code fixes. For e-commerce sites, this means accessible checkout flows that work for all customers. For development teams, Bouncer catches form issues before deployment.

Ready to fix your forms? Get a free accessibility scan to identify form accessibility issues across your site.


Related Articles:


Content disclosure: This article was produced using AI-assisted tools and reviewed by TestParty's team of accessibility specialists. As a company focused on source code remediation and continuous accessibility monitoring, we aim to share practical knowledge about WCAG and ADA compliance. That said, accessibility is complex and context-dependent. The information here is educational only—please work with qualified professionals for guidance specific to your organization's needs.

Stay informed

Accessibility insights delivered
straight to your inbox.

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo