Frontend Accessibility (WCAG 2.2)
Practical accessibility for Next.js applications.
Baseline Standards (WCAG 2.2 Level AA)
1. Semantic HTML
Use native HTML elements before ARIA. Correct semantics solve most accessibility issues.
// ❌ Div soup
<div role="button" tabIndex={0}>Click me</div>
// ✅ Native button
<button me</button>
// ❌ Nav as generic div
<div className="nav"><a href="/">Home</a></div>
// ✅ Use <nav>
<nav aria-label="Main"><a href="/">Home</a></nav>
2. Keyboard Navigation
Every interactive element must be reachable and operable via keyboard.
// Tab order — check these in order:
// 1. Do interactive elements have visible focus indicators?
// 2. Is the tab order logical (DOM order = focus order)?
// 3. Can all functionality be reached with Tab / Shift+Tab?
// 4. Can dropdowns/menus/modals be closed with Escape?
const focusRing = 'focus-visible:outline-2 focus-visible:outline-blue-500'
3. ARIA Labels
Every interactive element needs an accessible name.
// Icon buttons need labels
<button aria-label="Search">
<SearchIcon />
</button>
// Sections need labelled landmarks
<nav aria-label="Footer navigation">...</nav>
<aside aria-label="Related articles">...</aside>
Next.js Specific Considerations
Link Components
// Links need discernible text
<Link href="/articles/1" aria-label="Read article: How to build accessible apps">
Read more
</Link>
Image alt Text
// Decorative images: alt=""
<Image src="/bg.jpg" alt="" role="presentation" />
// Informative images: describe the content
<Image src="/chart.png" alt="Revenue chart: Q1 $10K, Q2 $15K, Q3 $12K" />
Form Validation
// Associate errors with inputs
<label htmlFor="email">Email</label>
<input
id="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <p id="email-error" role="alert">{errors.email}</p>}
Automated Testing
# Install axe-core Playwright integration
npm install -D @axe-core/playwright
# Run in CI as part of E2E tests
import { injectAxe, checkA11y } from 'axe-playwright'
test('homepage has no violations', async ({ page }) => {
await page.goto('/')
await injectAxe(page)
await checkA11y(page, null, {
includedImpacts: ['critical', 'serious']
})
})
Manual Testing Checklist
- Navigate all pages with Tab only — is everything reachable?
- Can all actions be completed without a mouse?
- Does every image have appropriate alt text?
- Are focus indicators visible on all interactive elements?
- Does zooming to 200% break layout?
- Can forms be submitted with Enter key?
- Do error messages have
role="alert"? - Is color contrast ≥ 4.5:1 for normal text, ≥ 3:1 for large text?
- Is the page navigable with a screen reader (VoiceOver / NVDA)?