Next.js Accessibility: Complete WCAG Guide for Developers
Next.js Accessibility: Complete WCAG Guide for Developers
Next.js is the most popular React framework for production web applications, powering everything from small business sites to large-scale SaaS platforms. Its built-in features — server-side rendering, image optimization, automatic code splitting — make it excellent for performance. But accessibility requires additional deliberate work.
This guide covers Next.js-specific accessibility patterns, the framework's built-in accessibility features, and how to audit your Next.js app for EAA compliance.
Next.js Built-in Accessibility Features
Route Announcements
Since Next.js 10, the framework includes a built-in route change announcer. When navigation occurs via the Next.js router, it announces the new page title to screen readers using a visually-hidden live region. This is one of the most important accessibility features for single-page apps.
This works automatically for the Pages Router. With the App Router (Next.js 13+), you need to ensure your page components have proper titles via the metadata export.
next/image
The next/image component requires an alt prop by default, which prevents the common mistake of missing alt text. However, it doesn't validate that the alt text is meaningful — that's your responsibility.
import Image from 'next/image';
// Good: descriptive alt text
<Image src="/product.jpg" alt="Blue leather wallet, bifold, 8 card slots" width={400} height={300} />
// Decorative image: empty alt
<Image src="/divider.png" alt="" width={800} height={4} />
next/link
The next/link component renders a standard <a> element, which is inherently accessible. However, ensure your link text is descriptive — not "click here" or "read more."
App Router Accessibility (Next.js 13+)
The App Router introduces server components and a new file-based routing system. Key accessibility considerations:
Page Titles with Metadata API
// app/products/[id]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id);
return {
title: `${product.name} | My Store`,
description: product.description,
};
}
export default function ProductPage({ params }) {
// ...
}
Unique, descriptive page titles are required by WCAG 2.4.2 (Page Titled, Level A). The metadata API makes this straightforward.
Focus Management in Server Components
Server components don't have access to browser APIs, so focus management must happen in client components. Use the "use client" directive for components that need to manage focus.
'use client';
import { useEffect, useRef } from 'react';
export function Modal({ isOpen, onClose, title, children }) {
const dialogRef = useRef(null);
useEffect(() => {
if (isOpen && dialogRef.current) {
dialogRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}
Common Next.js Accessibility Issues
1. Missing Language Declaration
WCAG 3.1.1 requires the language of the page to be programmatically determinable. Set the lang attribute on the <html> element.
In Next.js, configure this in next.config.js or next.config.ts:
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'pt', 'es', 'fr'],
defaultLocale: 'en',
},
};
Or set it directly in your root layout for the App Router:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
2. Heading Hierarchy Issues
With component-based architecture, it's easy to end up with multiple <h1> elements or broken heading hierarchies when composing layouts. Audit your rendered output — not just individual components.
3. Color Contrast in Tailwind Styles
Many Next.js projects use Tailwind CSS. Tailwind's default color palette includes many combinations that fail WCAG contrast requirements. For example, text-gray-400 bg-white has a contrast ratio of approximately 3.7:1 — insufficient for normal text (requires 4.5:1).
Use EAAPass to automatically detect all contrast failures across your site.
4. Dynamic Imports and Lazy Loading
Next.js's next/dynamic for code splitting can cause accessibility issues if loading states aren't managed properly. Ensure loading skeletons or spinners have appropriate ARIA labels.
Testing Next.js Apps for Accessibility
Because Next.js apps are JavaScript-rendered, you need tools that execute JS before testing:
- EAAPass — renders your deployed Next.js app with a headless browser and tests all 50 WCAG 2.1 AA criteria. Free, no signup required.
- jest-axe — unit test components with axe-core
- Playwright with
@axe-core/playwright— integration tests with full JS execution - Chrome DevTools Accessibility Inspector — manual testing tool
Next.js + EAAPass: Practical Workflow
- Deploy your Next.js app (even to a staging environment)
- Run a free audit at eaapass.eu
- Review the prioritized violation list — Critical and Serious issues first
- Fix issues in your components
- Re-audit to verify fixes
- Generate your EAA compliance certificate and Accessibility Statement
This workflow integrates naturally with Next.js development cycles and gives you the documentation trail needed for EAA compliance.