Skip to main content

Next.js Accessibility Best Practices (2026)

Benjamin Morton

Next.js is the most accessible way to build a React app, at least on average. In 2026, pages built with Next.js averaged 40.9 detectable accessibility errors, 27.1% below the all-site average of 56.1 and the best of any major framework WebAIM measured (WebAIM Million, 2026). That's the good news. The catch: 40.9 is still 40 errors per page, and the framework's best defaults don't touch the one thing that breaks accessibility in every single-page app.

Next.js gives you a route announcer, a required alt prop on images, real anchors from next/link, and a Metadata API for page titles. Used well, those close most of the common gaps. But it does not manage focus when you navigate, and that omission strands keyboard and screen reader users on every route change. This guide covers the build patterns that make a Next.js App Router app genuinely accessible, and the gap you have to fill yourself. For the testing side, see our React accessibility testing guide.

TL;DR: Next.js handles some accessibility for free: it announces route changes, requires image alt text, and renders real links. It does not set your lang, write your titles, add a skip link, or move focus on navigation. Set lang in the root layout, give every route a title via the Metadata API, add a skip link and landmarks once in app/layout.tsx, and manage focus on route change yourself. Next.js pages still average 40.9 errors (WebAIM Million, 2026), so the defaults aren't enough on their own.

How Accessible Is Next.js Out of the Box?

Better than raw React, but far from done. Next.js pages averaged 40.9 errors in 2026 against a 56.1 average (WebAIM Million, 2026), because several good defaults ship in the box. The App Router, default since Next.js 16 (released October 2025), includes a route announcer that tells assistive technology when a client-side navigation happens (Next.js, 2026).

That announcer looks for the new page's name in a specific order: document.title first, then the <h1>, then the URL pathname (Next.js, 2026). Add next/link (which renders a real <a>, keeping keyboard and focus behavior) and next/image (whose alt prop is marked required in the API), and you've avoided a few of the most common failures without writing extra code. The rest is on you. Here's the split at a glance:

Handled by Next.js by default

On you to add

Announcing route changes to assistive tech

Moving focus on route change

alt as a required prop on next/image

The lang attribute on <html>

Real, focusable anchors via next/link

A unique <title> for every route

Server-rendered HTML for assistive tech

A skip link and landmark structure

Citation capsule: In 2026, home pages built with Next.js averaged 40.9 detectable accessibility errors, 27.1% below the all-site average of 56.1 and the lowest of any major JavaScript framework WebAIM measured (WebAIM Million, 2026). Next.js ships a default route announcer, a required image alt prop, and real anchor rendering via next/link, but leaves language, titles, skip links, and focus management to the developer.

The Gap Next.js Won't Fill: Focus on Navigation

Here's the trap that catches almost every Next.js app. When you navigate with next/link, Next.js announces the new page through a live region, but it does not move focus (Next.js, 2026). Focus stays on the link you just clicked, which is often now gone from the page. A keyboard user's next Tab starts from nowhere useful, and it's a long-standing open request (vercel/next.js#49386, 2026).

Announcing is not the same as moving focus. The fix is a small client hook that watches the pathname and sends focus to the new page's heading after each navigation:

'use client';
import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';

export function useFocusOnNavigation() {
  const pathname = usePathname();
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    const heading = document.querySelector('h1');
    if (heading) {
      heading.setAttribute('tabindex', '-1');
      (heading as HTMLElement).focus();
    }
  }, [pathname]);
}

Call it from a client component near the root of your app. The isFirstRender guard skips the initial load, where focus is already correct, and only intervenes on subsequent route changes. Setting tabindex="-1" makes the heading focusable without adding it to the tab order. This is the single highest-impact accessibility fix in a Next.js codebase, and no scanner will flag its absence.

Set the Language and Page Titles

A missing lang attribute showed up on 13.5% of home pages in 2026, one of the most common failures on the web, and a missing or generic page title is just as easy to ship (WebAIM Million, 2026). Next.js fixes neither for you, but both are one-time setup. The lang attribute is manual: you write it on the <html> element in your root layout, which the App Router requires you to define. Titles go through the Metadata API.

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: { default: 'a11yFlow', template: '%s | a11yFlow' },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

The template field appends your site name to every page title automatically. Then each route sets just its own part:

// app/pricing/page.tsx
export const metadata = { title: 'Pricing' }; // renders "Pricing | a11yFlow"

Unique titles do double duty here: they satisfy WCAG 2.4.2 Page Titled, and they give the route announcer the descriptive name it reads on navigation. One pattern fixes two things.

Add a Skip Link and Landmarks Once, in the Layout

Because the App Router's root layout wraps every page, it's the right place to add structure that should appear everywhere: a skip link and semantic landmarks. Do it once and every route inherits it. A skip link lets keyboard users bypass the navigation (WCAG 2.4.1), and landmarks like <main> and <nav> give screen reader users one-key jumps between regions (WCAG 1.3.1).

// app/layout.tsx (body contents)
<body>
  <a href="#main-content" className="skip-link">Skip to content</a>
  <header>
    <nav>{/* site navigation */}</nav>
  </header>
  <main id="main-content">{children}</main>
  <footer>{/* site footer */}</footer>
</body>
.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: 1rem;
  top: 1rem;
}

The skip link's target, #main-content, matches the id on <main>. It stays offscreen until a keyboard user tabs to it. Putting the landmarks in the layout rather than in each page guarantees a consistent structure that assistive technology can rely on across the whole app.

A code editor showing a Next.js layout file with semantic HTML landmarks

Use next/link and next/image the Right Way

Reach for next/link on every internal navigation, and resist the urge to fake it with a clickable <div>. next/link renders a real anchor, so it's keyboard-focusable, announced as a link, and it triggers the route announcer. A div with an onClick gets none of that (WCAG 2.1.1). Save useRouter().push() for genuinely programmatic navigation, like redirecting after a form submit.

import Link from 'next/link';
import Image from 'next/image';

// ✅ real anchor: keyboard-operable, announced, triggers the route announcer
<Link href="/pricing">View pricing</Link>

// ✅ alt is a required prop on next/image; describe what it conveys
<Image src="/revenue.png" alt="Revenue grew 42% in 2026" width={800} height={400} />

// ✅ decorative image: empty alt so screen readers skip it
<Image src="/flourish.svg" alt="" width={40} height={40} />

Because next/image marks alt as required, the component nudges you toward the right habit (Next.js, 2026). Use that: write alt that carries meaning, and set alt="" for purely decorative images so they're hidden from assistive technology. That directly addresses missing alt text, WebAIM's second most common failure at 53.1% of pages (WebAIM Million, 2026).

How Do You Test Next.js Accessibility?

Build-time patterns get you most of the way; testing catches what slips through. One note specific to Next.js: it used to bundle accessibility linting through the next lint command, but that command was removed in Next.js 16, so add eslint-plugin-jsx-a11y to your ESLint config yourself (Next.js, 2025). From there, run axe-core against rendered pages in CI.

Automated tools catch about 57% of accessibility issues by volume (Deque, 2021), and only about 13% of the 55 WCAG 2.2 AA success criteria with high accuracy (Accessible.org, 2025). Focus management on route change, the biggest Next.js gap, is squarely in the part no scanner reaches. It's also the kind of gap accessibility overlays can't fix, whatever they promise, which is why the manual test matters: navigate, then check where focus landed. Our React accessibility testing guide covers the full toolchain, and the GitHub Actions setup guide wires it into CI.

What we see: The Next.js apps that pass a full axe scan and still fail real users almost always fail on focus after navigation. The scanner is happy because the DOM is valid; the keyboard user is lost because focus never moved. Test it by hand on every route change, because it's the one thing the automated 57% can't see.

Frequently Asked Questions

Is Next.js accessible by default?

Partly. Next.js ships a route announcer, requires alt on next/image, and renders real anchors via next/link, which is why Next.js pages average 40.9 errors versus the 56.1 site average (WebAIM Million, 2026). But it doesn't set lang, write titles, add skip links, or manage focus. Those are on you.

Does Next.js handle focus on route changes?

No. Next.js announces the new page through a live region but does not move focus on client-side navigation (Next.js, 2026). Focus stays on the clicked link, stranding keyboard and screen reader users. You need a small client hook that moves focus to the new page's heading after each navigation.

How do I set the page title in Next.js for accessibility?

Use the Metadata API. Export a metadata object (or generateMetadata) from each page.tsx, and set a title.template in the root layout so your site name appends automatically. Unique titles satisfy WCAG 2.4.2 and feed the route announcer, which reads document.title first (Next.js, 2026).

Where do I put a skip link in the Next.js App Router?

In the root layout (app/layout.tsx), as the first element inside <body>, so it applies to every route. Point its href at the id on your <main> landmark, satisfying WCAG 2.4.1 Bypass Blocks. Next.js won't add one for you, so it's part of the work beyond the 57% of issues automation catches (Deque, 2021).

Does Next.js include accessibility linting?

Not any more by default. The next lint command that bundled eslint-plugin-jsx-a11y was removed in Next.js 16 (Next.js, 2025). Add the plugin to your ESLint config yourself. It catches authoring mistakes like a missing alt prop, but not rendered issues like focus order, which need axe-core and manual testing.

The Bottom Line

Next.js earns its 40.9-error average, the best of any framework in WebAIM's 2026 data, by shipping accessible defaults: a route announcer, required image alt text, and real anchors (WebAIM Million, 2026). But 40.9 is not zero, and the defaults stop short of the essentials. Set lang in the root layout, title every route through the Metadata API, add a skip link and landmarks once in the layout, and prefer next/link and next/image.

Then close the gap Next.js leaves open: move focus on route change yourself, because the framework only announces and no scanner will catch the omission. Test the rest with the toolchain in our React accessibility testing guide, fix what it finds with the common WCAG violations guide, and read the developer's guide to web accessibility testing for the full picture.


Sources