Next.js gives you fast, server-rendered pages out of the box, but Next.js accessibility is still your job. The framework renders whatever markup you write, so a missing lang attribute, an icon button with no name, or a decorative image with a meaningless alt will ship straight to production unless you catch it.
This guide walks through the accessibility decisions specific to building with the App Router: the document lang attribute, page titles and metadata, links and focus on navigation, images and alt text, and the tradeoffs between Server and Client Components. Every example uses current App Router conventions.
Set the lang attribute in the root layout
Screen readers choose pronunciation rules from the document language. Without it, an English page may be read with French phonetics, or the screen reader falls back to the user's system language and gets it wrong. WCAG 3.1.1 (Language of Page) makes this a Level A requirement, so it is part of the baseline the European Accessibility Act references through EN 301 549.
In the App Router there is no _document.js. You set the language on the <html> element in your root app/layout.tsx:
export default function RootLayout({ children }) { return (<html lang="en"><body>{children}</body></html>); }
For internationalized sites, drive lang from the active locale instead of hardcoding it, so a French route renders <html lang="fr">. When only a phrase differs from the page language, mark that fragment directly, for example <span lang="es">.
Give every page a real title with the Metadata API
The page title is the first thing a screen reader announces on load and the label users see when scanning browser tabs. WCAG 2.4.2 (Page Titled, Level A) requires every page to have a descriptive, unique title. In Next.js, export a metadata object or generateMetadata from any layout or page rather than rendering a <title> tag yourself.
export const metadata = { title: 'Checkout - Acme Store', description: 'Review your order and pay securely.' };
Use a title template in the root layout so each page contributes a specific name without repeating the brand, for example title: { template: '%s | Acme', default: 'Acme' }. Avoid identical titles like 'Home' on five routes; that defeats the purpose for anyone navigating by tab or history.
Headings are separate from the title. Each page needs exactly one <h1> that matches the page's purpose, with no skipped levels below it. The title and the <h1> can share wording but serve different surfaces.
Links, navigation, and focus
next/link renders a real <a> element, so keyboard users can Tab to it and activate it with Enter, and assistive tech treats it as a link. That is the right default. Problems start when developers fight the platform.
- Do not put an onClick on a <div> to fake a link or button. Use <Link> for navigation and <button> for actions so keyboard and screen-reader support come for free.
- Never remove focus outlines with outline: none and nothing in their place. Provide a visible :focus-visible style instead. WCAG 2.2 added 2.4.11 Focus Not Obscured (Minimum, AA), so the focused element must also stay unobstructed by sticky headers or cookie bars.
- Give links meaningful text. A page full of 'click here' or 'read more' links is useless out of context. If the visible text must stay short, add an aria-label that names the destination.
- Hit targets should be at least 24 by 24 CSS pixels under WCAG 2.5.8 Target Size (Minimum, AA), which matters for icon-only nav buttons.
For client-side route changes in app-like interfaces, focus can get stranded on the old page. After navigating, move focus to the new <h1> and consider a polite live region that announces the new page name, so screen-reader users know the view changed. Content-style sites with full document transitions usually do not need this, but dashboards and multi-step flows do. Our keyboard accessibility guide covers focus management patterns in depth.
Images and alt text with next/image
The next/image component optimizes images but does not write alt text for you, and the alt prop is required for a reason. WCAG 1.1.1 (Non-text Content, Level A) means every image needs a text alternative appropriate to its role.
- Informative images: describe the information, not the file. alt="Quarterly revenue up 30 percent" beats alt="chart.png".
- Decorative images: pass an empty string, alt="". This tells screen readers to skip it. Omitting the prop entirely is wrong; some tools then read the filename aloud.
- Images of text: avoid them where possible, and if unavoidable, put the exact text in alt.
- Icons that act as the only label for a control need an accessible name on the control, via aria-label or visually hidden text, not just an image.
Writing good alternatives is a skill of its own; see how to write alt text for the decision tree. The same care applies to background images set in CSS, which carry no alt at all, so any meaning they convey must appear elsewhere in the markup.
Server vs Client Components: where a11y bugs live
Server Components render semantic HTML that works before any JavaScript executes, which benefits screen readers, keyboard users, and anyone on a slow connection. That is a genuine accessibility win, but it does not make a component accessible on its own; bad markup is bad markup on the server too.
Most accessibility defects appear in Client Components, the parts marked with 'use client': custom dropdowns, modals, tabs, tooltips, and anything that updates the DOM after load. These need deliberate keyboard support, ARIA roles and states, and focus management. A dialog must trap focus while open, return focus to the trigger on close, and be dismissible with Escape. Content that appears dynamically should be announced through a live region rather than silently inserted.
A practical rule: use the platform first and stay on the server where you can; reach for a Client Component only when you need interactivity, then test that piece hardest. Before reinventing a widget, prefer native elements, since the first rule of ARIA is not to use ARIA when HTML already does the job.
Test it, do not assume it
Static analysis with eslint-plugin-jsx-a11y catches a class of issues at build time, such as an <img> with no alt or an anchor with no href, and it is worth wiring into CI. But linting only sees your source, not the rendered, interactive result.
Run an automated scan against your deployed URL with a free checker like AccessScan to catch missing names, contrast problems, and structure errors across the rendered page. Automated tooling reaches only about a third of WCAG issues, so follow it with the manual basics: Tab through every interactive element, confirm a visible focus ring, and listen to a page or two with VoiceOver or NVDA.
If you are shipping into the EU market, the European Accessibility Act has applied since 28 June 2025 and the baseline is WCAG 2.2 Level A and AA. Before launch, work through our dedicated Next.js accessibility guide for component-level patterns and run a final pass with the keyboard and a screen reader.