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-invalidtells screen readers field has erroraria-describedbylinks error text to fieldrole="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
| Tool | Catches | Misses | Best For |
|---|---|---|---|
| Axe DevTools | Structure (labels, ARIA) | UX clarity | Fast feedback in dev |
| axe-core CLI | Same as DevTools | UX clarity | CI/CD automation |
| Sa11y (AI) | Visual confusion, misleading text | Complex interactions | Pre-production review |
| Manual testing | Real user experience | Nothing | Final 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:
- Added explicit
<label>for each field - Linked hints with
aria-describedby - Error container ready with
role="alert" - Button has clear action text
- 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
| Key | Expected Behavior |
|---|---|
| Tab | Move to next field |
| Shift+Tab | Move to previous field |
| Enter | Submit form |
| Space | Toggle checkbox/radio |
| Escape | Close modal, clear autocomplete |
| Arrow keys | Move through radio group |
Tested with Axe DevTools 4.8, Sa11y 3.2, NVDA 2024.1, VoiceOver (macOS 14), Chrome 121, Firefox 123