Form Accessibility Testing: Labels, Errors, and Hints — practical accessibility testing techniques.
Form Accessibility Testing: Labels, Errors, and Hints is an essential part of WCAG 2.1 AA compliance. This lesson covers the techniques, tools, and test patterns needed to verify accessible implementations in production React applications.
Testing form accessibility testing: labels, errors, and hints requires a systematic approach combining automated tools (axe-core, Lighthouse) with manual verification (keyboard testing, screen reader review).
1import { render, screen } from '@testing-library/react';
2import { axe, toHaveNoViolations } from 'jest-axe';
3
4expect.extend(toHaveNoViolations);
5
6describe('Form Accessibility Testing: Labels, Errors, and Hints', () => {
7 it('meets WCAG 2.1 AA requirements', async () => {
8 const { container } = render(<YourComponent />);
9 const results = await axe(container, {
10 runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },
11 });
12 expect(results).toHaveNoViolations();
13 });
14
15 it('has proper semantic structure', () => {
16 render(<YourComponent />);
17 // Verify landmarks, headings, and semantic elements
18 expect(screen.getByRole('main')).toBeInTheDocument();
19 expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
20 });
21});Key implementation patterns for form accessibility testing: labels, errors, and hints:
1// Accessible component pattern
2function AccessibleComponent({ label, error }: Props) {
3 const inputId = useId();
4 const errorId = useId();
5
6 return (
7 <div>
8 <label htmlFor={inputId}>{label}</label>
9 <input
10 id={inputId}
11 aria-describedby={error ? errorId : undefined}
12 aria-invalid={error ? 'true' : undefined}
13 />
14 {error && (
15 <span id={errorId} role="alert">
16 {error}
17 </span>
18 )}
19 </div>
20 );
21}End-to-end accessibility tests with Playwright:
1import { test, expect } from '@playwright/test';
2import AxeBuilder from '@axe-core/playwright';
3
4test('form accessibility testing: labels, errors, and hints meets WCAG AA', async ({ page }) => {
5 await page.goto('/your-page');
6 await page.waitForLoadState('networkidle');
7
8 const results = await new AxeBuilder({ page })
9 .withTags(['wcag2a', 'wcag2aa'])
10 .analyze();
11
12 const critical = results.violations.filter(
13 v => v.impact === 'critical' || v.impact === 'serious'
14 );
15
16 if (critical.length > 0) {
17 critical.forEach(v => console.error('[' + v.impact + '] ' + v.description));
18 }
19
20 expect(critical).toHaveLength(0);
21});Integrate form accessibility testing: labels, errors, and hints into CI:
1# .github/workflows/a11y.yml
2name: Accessibility Tests
3on: [push, pull_request]
4jobs:
5 a11y:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with: { node-version: '20', cache: 'npm' }
11 - run: npm ci
12 - run: npm run test:a11y
13 - uses: actions/upload-artifact@v4
14 if: always()
15 with:
16 name: a11y-results
17 path: test-results/Most common form accessibility testing: labels, errors, and hints violations and their fixes:
1// Violation: Missing form label
2// BAD:
3<input type="email" placeholder="Email" />
4
5// GOOD:
6<label htmlFor="email">Email address</label>
7<input id="email" type="email" aria-required="true" />
8
9// Violation: Non-descriptive link text
10// BAD:
11<a href="/courses/1">Read more</a>
12
13// GOOD:
14<a href="/courses/typescript-mastery">
15 View TypeScript Mastery course
16</a>
17
18// Violation: Color as only differentiator
19// BAD:
20<span style={{ color: 'red' }}>Error</span>
21
22// GOOD:
23<span style={{ color: 'red' }} role="alert">
24 <svg aria-hidden="true"><path /></svg>
25 Error: Invalid email format
26</span>A working test implementation covering automated and manual accessibility testing patterns.
1// tests/a11y/accessible-forms.spec.ts
2import { test, expect } from '@playwright/test';
3import AxeBuilder from '@axe-core/playwright';
4
5test.describe('Form Accessibility Testing: Labels, Errors, and Hints', () => {
6 test('no critical axe violations', async ({ page }) => {
7 await page.goto('/');
8 await page.waitForLoadState('networkidle');
9
10 const results = await new AxeBuilder({ page })
11 .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
12 .analyze();
13
14 const critical = results.violations.filter(
15 v => v.impact === 'critical' || v.impact === 'serious'
16 );
17 expect(critical, 'No critical/serious a11y violations').toHaveLength(0);
18 });
19
20 test('keyboard navigation works', async ({ page }) => {
21 await page.goto('/');
22 // First Tab stop should be skip link
23 await page.keyboard.press('Tab');
24 const isSkipLink = await page.evaluate(() =>
25 document.activeElement?.textContent?.toLowerCase().includes('skip') ||
26 (document.activeElement as HTMLAnchorElement)?.href?.includes('#main')
27 );
28 expect(isSkipLink).toBe(true);
29 });
30
31 test('all images have alt text', async ({ page }) => {
32 await page.goto('/courses');
33 const missing = await page.evaluate(() =>
34 Array.from(document.querySelectorAll('img:not([aria-hidden])'))
35 .filter(img => !(img as HTMLImageElement).alt)
36 .map(img => (img as HTMLImageElement).src)
37 );
38 expect(missing, 'All images must have alt text').toHaveLength(0);
39 });
40
41 test('color contrast meets WCAG AA (4.5:1)', async ({ page }) => {
42 await page.goto('/');
43 const results = await new AxeBuilder({ page })
44 .withRules(['color-contrast'])
45 .analyze();
46 expect(results.violations).toHaveLength(0);
47 });
48});This test suite covers the four key form accessibility testing: labels, errors, and hints areas: automated axe-core scanning for structural violations, keyboard navigation verification, image alt text completeness, and color contrast checking. The axe-core approach catches 30-40% of WCAG issues automatically; the remaining violations require the manual keyboard testing and screen reader verification covered in subsequent lessons.
Continue with the next accessibility topic to build a comprehensive WCAG 2.1 AA test suite.
Your progress is saved in this browser, on this device only.