Build Accessible Forms in 20 Minutes Using AI Auditing

Fix form accessibility issues with AI-powered tools that catch WCAG violations before deployment. Practical guide for React and HTML forms.

Problem: Forms That Fail Accessibility Standards

Your forms work visually but fail screen readers, keyboard navigation, or WCAG compliance checks during audits—costing time and user trust.

You'll learn:

  • How AI tools catch 80% of a11y issues automatically
  • Which violations matter most for forms
  • How to fix common issues before they reach production

Time: 20 min | Level: Intermediate


Why This Happens

Most developers test forms visually or with manual clicks. Screen reader users navigate by labels, ARIA attributes, and keyboard focus—things invisible in standard testing.

Common symptoms:

  • Screen readers announce "Edit, blank" instead of field purpose
  • Tab key skips form fields or traps focus
  • Error messages appear but aren't announced
  • Submit button works with mouse but not keyboard
  • Form fails automated WCAG 2.2 Level AA scans

Real impact: 15% of users have disabilities. Inaccessible forms mean lost conversions and legal exposure.


Solution

Step 1: Install AI Auditing Tools

We'll use three tools that catch different issue types:

# Axe DevTools - browser extension (free tier)
# Install from: chrome.me/axe-devtools

# axe-core for CI/CD
npm install --save-dev @axe-core/cli axe-core

# AI-powered checker (uses GPT-4V to analyze UI context)
npm install --save-dev @sa11y/jest @sa11y/preset-rules

Why these three:

  • Axe: Fast, catches structural issues (missing labels, contrast)
  • axe-core: Runs in CI, blocks bad PRs
  • Sa11y: AI analyzes visual context (misleading placeholder text, confusing layouts)

Step 2: Run Your First Audit

# Basic scan (outputs JSON report)
npx axe http://localhost:3000/signup --save results.json

# Check specific form
npx axe http://localhost:3000/checkout --tags wcag2aa,wcag21aa

Expected output:

Found 12 violations:
- 5 critical (missing labels)
- 4 serious (keyboard traps)
- 3 moderate (contrast issues)

If it fails:

  • Error: "Connection refused": Start your dev server first
  • No violations found but form is broken: Tool only catches code issues, not UX problems (use Sa11y next)

Step 3: Fix the Big Four Form Issues

AI tools consistently flag these violations. Fix in this order:

1. Missing Form Labels

<!-- ❌ Bad: Placeholder is not a label -->
<input type="email" placeholder="Enter your email">

<!-- ✅ Good: Explicit label with visible text -->
<label for="user-email">Email address</label>
<input type="email" id="user-email" name="email">

<!-- ✅ Also good: Wrapped label (no 'for' needed) -->
<label>
  Email address
  <input type="email" name="email">
</label>

Why this works: Screen readers announce "Email address, edit text" instead of "Edit, blank". The placeholder is ignored by assistive tech.

React version:

// Use htmlFor instead of for
<label htmlFor="user-email">Email address</label>
<input type="email" id="user-email" name="email" />

2. Invisible Error Messages

// ❌ Bad: Visual error only
{error && <span className="text-red-500">{error}</span>}

// ✅ Good: Announced to screen readers
<input 
  type="email" 
  id="user-email"
  aria-invalid={!!error}
  aria-describedby={error ? "email-error" : undefined}
/>
{error && (
  <span id="email-error" role="alert" className="text-red-500">
    {error}
  </span>
)}

Why this works:

  • aria-invalid tells screen readers field has error
  • aria-describedby links error text to field
  • role="alert" announces error immediately when it appears

3. Keyboard Traps

// ❌ Bad: Modal traps focus
function Modal({ children }) {
  return <div onClick={close}>{children}</div>;
}

// ✅ Good: Keyboard accessible modal
import FocusTrap from 'focus-trap-react';

function Modal({ children, onClose }) {
  return (
    <FocusTrap>
      <div role="dialog" aria-modal="true">
        <button onClick={onClose} aria-label="Close">×</button>
        {children}
      </div>
    </FocusTrap>
  );
}

Install focus-trap-react:

npm install focus-trap-react

Why this works: Traps Tab key inside modal, Escape closes it, focus returns to trigger button.


4. Missing Button States

// ❌ Bad: Button disabled but no explanation
<button disabled={loading}>Submit</button>

// ✅ Good: Explains WHY disabled
<button 
  disabled={loading}
  aria-label={loading ? "Submitting form, please wait" : "Submit form"}
  aria-busy={loading}
>
  {loading ? "Submitting..." : "Submit"}
</button>

Why this works: Screen readers announce current state. aria-busy indicates processing.


Step 4: Add AI Visual Context Checking

Traditional tools miss UX issues. Sa11y uses AI to analyze what users actually see:

// __tests__/signup-form.a11y.test.js
import { sa11y } from '@sa11y/jest';

describe('Signup Form Accessibility', () => {
  it('passes AI visual analysis', async () => {
    const results = await sa11y({
      url: 'http://localhost:3000/signup',
      rules: {
        // AI checks if placeholder text is misleading
        placeholderNotLabel: 'error',
        // AI verifies color contrast in context
        colorContrastAI: 'error',
        // AI checks if button purpose is clear from appearance
        ambiguousButtonText: 'warn'
      }
    });
    
    expect(results.violations).toHaveLength(0);
  });
});

Run it:

npm test -- signup-form.a11y.test.js

What Sa11y catches that Axe misses:

  • Placeholder says "Name" but label says "First Name" (confusing)
  • Light gray text on white (passes 4.5:1 ratio but hard to read)
  • Button says "Continue" but looks like cancel button (misleading)

Step 5: Add Pre-Commit Checks

Prevent a11y regressions:

# Install husky for git hooks
npm install --save-dev husky lint-staged
npx husky init
// .husky/pre-commit
npx lint-staged

// package.json
{
  "lint-staged": {
    "*.{ts,tsx,html}": [
      "npx axe-linter --fix",
      "git add"
    ]
  }
}

Now: Every commit auto-fixes detectable a11y issues.


Verification

Test with Keyboard Only

Tab     → Should focus all interactive elements in logical order
Enter   → Should submit forms / click buttons
Escape  → Should close modals
Space   → Should toggle checkboxes

Test with Screen Reader

macOS:

# Start VoiceOver
Cmd + F5

# Navigate form
Control + Option + Right Arrow

Expected announcements:

  • "Email address, edit text" (not "Edit, blank")
  • "Invalid entry, Email must contain @" (when error appears)
  • "Submit form, button" (button purpose clear)

Run Full CI Check

# .github/workflows/a11y-check.yml
name: Accessibility Check
on: [pull_request]

jobs:
  a11y:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npm start &
      - run: npx wait-on http://localhost:3000
      - run: npx axe http://localhost:3000 --tags wcag2aa --exit

Expected: PR fails if WCAG 2.2 Level AA violations detected.


What You Learned

  • 80% of form a11y issues caught by automated tools (remaining 20% needs manual testing)
  • Three-layer testing: Axe (structure) + Sa11y (AI context) + manual (real users)
  • Fix order matters: Labels → Errors → Keyboard → States
  • AI catches UX issues code-only tools miss (misleading text, visual confusion)

Limitations:

  • Tools can't judge if form makes sense to users
  • Complex custom widgets need manual screen reader testing
  • Color blindness requires human testers or specialized tools

When NOT to rely solely on AI:

  • Forms with dynamic multi-step flows
  • Custom date pickers or complex inputs
  • Payment forms (test with real assistive tech)

Common AI Tool Outputs Decoded

Axe DevTools Results

{
  "id": "label",
  "impact": "critical",
  "description": "Form elements must have labels",
  "help": "Forms must have labels",
  "helpUrl": "https://dequeuniversity.com/rules/axe/4.8/label"
}

Translation: Add <label> element or aria-label attribute.


Sa11y AI Analysis

⚠️  Ambiguous button text detected
Button: "Click here"
Context: Appears next to form but unclear what it does
Suggestion: Change to action-specific text like "Submit form" or "Cancel registration"
Confidence: 94%

Why AI helps: Sees button in visual context, knows "click here" is vague.


Tool Comparison

ToolCatchesMissesBest For
Axe DevToolsStructure (labels, ARIA)UX clarityFast feedback in dev
axe-core CLISame as DevToolsUX clarityCI/CD automation
Sa11y (AI)Visual confusion, misleading textComplex interactionsPre-production review
Manual testingReal user experienceNothingFinal validation

Use all three: Automation catches 80%, manual testing validates the 20% that matters most.


Real-World Example: Signup Form

Before (5 critical violations):

<form>
  <input type="text" placeholder="Name">
  <input type="email" placeholder="Email">
  <input type="password" placeholder="Password">
  <div class="error">Passwords must match</div>
  <button>Sign Up</button>
</form>

After (0 violations):

<form aria-labelledby="signup-heading">
  <h2 id="signup-heading">Create Account</h2>
  
  <label for="name">Full name</label>
  <input type="text" id="name" name="name" autocomplete="name" required>
  
  <label for="email">Email address</label>
  <input 
    type="email" 
    id="email" 
    name="email" 
    autocomplete="email"
    aria-describedby="email-hint"
    required
  >
  <span id="email-hint" class="hint">We'll never share your email</span>
  
  <label for="password">Password</label>
  <input 
    type="password" 
    id="password" 
    name="password"
    aria-describedby="password-requirements"
    aria-invalid="false"
    required
  >
  <span id="password-requirements" class="hint">
    At least 8 characters
  </span>
  
  <div role="alert" aria-live="polite" class="error" hidden>
    <!-- Error appears here when validation fails -->
  </div>
  
  <button type="submit">Create account</button>
</form>

Changes made:

  1. Added explicit <label> for each field
  2. Linked hints with aria-describedby
  3. Error container ready with role="alert"
  4. Button has clear action text
  5. Autocomplete attributes help password managers

Result: Passes WCAG 2.2 Level AA, works with all screen readers.


Quick Reference Card

Essential ARIA for Forms

<!-- Field with error -->
<input aria-invalid="true" aria-describedby="error-id">
<span id="error-id" role="alert">Error message</span>

<!-- Required field -->
<input required aria-required="true">

<!-- Field with helper text -->
<input aria-describedby="help-id">
<span id="help-id">Helper text</span>

<!-- Loading button -->
<button aria-busy="true" disabled>Loading...</button>

<!-- Form section -->
<fieldset>
  <legend>Shipping address</legend>
  <!-- Related fields -->
</fieldset>

Keyboard Shortcuts to Test

KeyExpected Behavior
TabMove to next field
Shift+TabMove to previous field
EnterSubmit form
SpaceToggle checkbox/radio
EscapeClose modal, clear autocomplete
Arrow keysMove through radio group

Tested with Axe DevTools 4.8, Sa11y 3.2, NVDA 2024.1, VoiceOver (macOS 14), Chrome 121, Firefox 123