Two app windows after one route change. With no route-change code, focus stays on the third of 12 header links, the screen reader says nothing and the new content is 10 Tab presses away. Patched, focus lands on the h1, it reads Order details, Your account, and the content is 1 Tab away.

Accessible single-page applications: what each router does on a route change, and how to fix the gap

A link click in an app with a client-side router looks like a new page and sounds like nothing at all. Here is what each popular router does about that, and the small patch that closes the gap.

What do accessible single-page applications have to do on every route change?#

Accessible single-page applications do two jobs on every client-side route change that a full page load used to do for free: announce the new page and move focus into it. A paper posted to arXiv on 19 February 2026 names the cause: in Angular apps, "client-side routing does not trigger a full-page reload". As a result, screen reader users have to manage focus to avoid getting lost.

The SvelteKit accessibility docs describe what the old page load did. On a server-rendered site, the screen reader reads the new page's title. Also, every navigation resets focus to the top of the page. However, a router that swaps the view in place does neither. The URL and the content change, yet the screen reader stays silent. Meanwhile, focus stays on the link that was clicked, or drops to the body when that link is removed.

Two jobs the router dropsA full page load announces the title and resets focus; a client-side route change does neither until router code does both. Source: SvelteKit accessibility docs, read October 2026.

So the whole fix for accessible single-page applications has two parts. First, give each route a title and make sure something reads it out. Second, move focus to a sensible place in the new view. Everything else in this guide is detail on those two jobs, plus the status messages that appear inside a view.

What does a silent route change cost the people using your app?#

In WebAIM's Screen Reader User Survey #11, 67.8% of respondents said they navigate by headings first on a long page, so focus stranded on a stale link breaks their first move. The survey ran from July to August 2026 and drew 1,780 valid responses.

Headings are the main road for most screen reader users, so focus stranded in the header blocks that road.

Show data table
What respondents are most likely to do first on a lengthy web page, in percent. Source: WebAIM Screen Reader User Survey #11, July to August 2026.
Item Value
Navigate through the headings 67.8%
Use the Find feature 12.5%
Read through the page 9.7%
Navigate through the links 6.7%
Navigate through the landmarks or regions 3.4%

Headings come first for two in three screen reader users.

What respondents do first What respondents are most likely to do first on a lengthy web page, in percent. Source: WebAIM Screen Reader User Survey #11, July to August 2026. WebAIM Screen Reader User Survey #11, July to August 2026

The cost for keyboard users is easy to count. Say your persistent header holds 12 links and someone activates the third one. On a router that does nothing with focus, focus stays on that third link. Then it takes 10 Tab presses to reach the first link in the new content. Meanwhile, a reset to the body sends them through all 12 header links again. That is 13 presses to the first link in the content. In contrast, a focused main heading puts them one Tab from the new content. In this example, that difference repeats on every navigation, all day.

Tab presses to the new content

Set how many links your persistent header holds and which one was activated; it counts the Tab presses to the first link in the new content for three focus choices.

Your header

an assumption, change it

Counts only the header links before the new content; skip links and other focusable items are left out.

Focus left on the clicked link

10

Focus reset to the body
13
Focus on the new view's heading
1

An illustrative model of the worked example above, not a measurement.

What does your framework already do when the route changes?#

Next.js, SvelteKit and Nuxt can announce a route change for you, while React Router, Angular and Vue Router leave both the announcement and the focus move to your code. The details differ, and two of them disagree outright.

The Next.js accessibility docs say the framework "includes a route announcer by default". Its App Router source creates a hidden region with aria-live set to assertive and role set to alert. But it does not move focus. The SvelteKit docs say it "injects a live region onto the page" that reads the title. It also focuses the body after each navigation. Nuxt's NuxtRouteAnnouncer is optional, arrived in version 3.12 and defaults to polite.

Meanwhile, the React Router docs say it "doesn't make any assumptions about your UI as the route changes". Angular updates the document title from each route's title, but focus is your job. Vue Router offers a scrollBehavior option for scroll position and nothing for focus.

What six routers do by default on a client-side route change: announce the new page, move focus, or neither. Source: Next.js, SvelteKit, Nuxt, React Router, Angular and Vue Router documentation and source, read October 2026.

RouterAnnounces the new pageMoves focus
Next.jsYes, by default, assertiveNo
SvelteKitYes, reads the titleYes, to the body
NuxtOpt-in component from 3.12, politeNo
React RouterNoNo
AngularSets the title onlyNo
Vue RouterNoNo

Source: each framework's own documentation and source, read October 2026.

The contradiction sits between SvelteKit and Angular. SvelteKit focuses the body by design. However, the Angular accessibility guide says "avoid situations where focus returns to the body element after a route change". Both cannot be the right target.

Where should focus go after a route change?#

Move focus to the new view's h1 with tabindex set to -1, because user testing found a focused heading the clearest signal of a new page. Gatsby ran user tests with people with disabilities in July 2019. There, "Focusing on a heading was found to be the best experience", since it saved time and made the change clear.

The same tests ranked the other targets lower. For example, focusing a wrapper around the new content worked but felt subtle, while a reset to the top felt overwhelming. In contrast, SvelteKit's body focus copies the old page load, which is safe but slow. So Angular's advice to avoid it is the better default.

The survey numbers point the same way, even though heading use slipped a little between the last two WebAIM surveys.

Show data table
Share of respondents who move by headings first on a lengthy page, and who use landmarks or regions always or often, in percent. Source: WebAIM Screen Reader User Surveys #10 and #11.
Dimension Survey #10, 2024 Survey #11, 2026
Headings first on a lengthy page 71.6% 67.8%
Landmarks or regions used always or often 31.8% 28.3%

Headings still lead by a wide margin, while landmark use falls.

Navigation habits, 2024 and 2026 Share of respondents who move by headings first on a lengthy page, and who use landmarks or regions always or often, in percent. Source: WebAIM Screen Reader User Surveys #10 and #11. WebAIM Screen Reader User Survey #10 (2024) and #11 (July to August 2026)

Because headings remain the main road while landmark use falls, the heading matches how people already move. The tabindex="-1" attribute lets a script focus the heading without adding it to the Tab order. Also, keep a visible focus ring on it, because a sighted keyboard user needs to see where focus went. The WCAG 2.2 Understanding page for Focus Order asks that focus moves in an order that keeps meaning. A focus point inside the new content does that.

How should the new page name be announced?#

Give every route a unique, descriptive document title, because the Next.js, SvelteKit and Nuxt announcers all read the title before anything else. The Next.js docs say its announcer "looks for the page name to announce by first inspecting document.title", then the h1, then the URL path.

What the announcer readsHow the Next.js route announcer picks the text it reads: document.title first, then the h1, then the URL path. Source: Next.js accessibility docs, read October 2026.

The WCAG 2.2 Understanding page for Page Titled sets the bar. Pages "have titles that describe topic or purpose." In practice, that means "Order details, Your account" and not "My App" on every view. Angular makes this a route setting, so each entry in the route definitions can carry a title. Also, a TitleStrategy can add a suffix across the app.

The politeness setting matters less than the title, but it still differs between frameworks. Next.js uses an assertive alert, while Nuxt defaults to polite. MDN's live regions guide says assertive "should only be used for time-sensitive/critical notifications". After all, it cuts off speech already under way. Therefore, polite is the safer choice when you build your own announcer.

When your router has no announcer, you have two options. First, you can move focus to the new heading, which makes the screen reader read it. Second, you can add one polite live region to the app shell and write the new title into it. Doing both is fine, as long as the two do not read the same text twice.

How do you announce loading, results and errors without a route change?#

An update inside a view, such as a result count or a save error, is a status message under WCAG 2.2 Success Criterion 4.1.3 and needs a live region already in the page. The Understanding page for Status Messages gives "18 results returned" as an example. It says such messages reach the user "without receiving focus."

So, keep route announcements and status messages apart. A route change is a change of context, and focus plus the title handle it. By contrast, a status message leaves the user where they are and only tells them something changed.

Each framework ships a helper for this. For example, Angular's CDK offers LiveAnnouncer. Its docs say it "is used to announce messages for screen-reader users using an aria-live region". The Angular guide also asks you to wrap @defer blocks in live regions, because lazy content may load in silence. Meanwhile, Nuxt points in-page changes to NuxtAnnouncer and the useAnnouncer composable instead of the route announcer.

Route change against status message: what each is, what handles it, and the framework helper. Source: WCAG 2.2 Understanding 4.1.3, Angular CDK and Nuxt documentation, read October 2026.

Route changeStatus message
What it isA change of contextA message that leaves the user where they are
What handles itFocus plus the titleA live region already in the page
Framework helperNuxtRouteAnnouncer, the Next.js and SvelteKit announcersAngular CDK LiveAnnouncer, NuxtAnnouncer and useAnnouncer

The one rule that catches most bugs is simple: the region must exist before the message arrives. In MDN's words, "Establish the live region before updating its content." The live regions guide covers the mechanics, from role="status" to aria-atomic, so they are not repeated here.

How much ARIA does a single-page application actually need?#

Less than most apps ship: WebAIM's February 2026 scan of one million home pages found pages with ARIA averaged 59.1 detected errors against 42 without. That works out to about 17 more potential barriers on each page that uses it.

Detected errors on home pages with and without ARIAabout 17 more

59.1

Home pages with ARIA

42

Home pages without ARIA

More ARIA, more detected errors, on pages that are also more complex.

Show data table
Detected errors on home pages with and without ARIA (average detected errors per home page)
Optionaverage detected errors per home page
Home pages with ARIA59.1
Home pages without ARIA42

Source: WebAIM Million, February 2026

The WebAIM Million report adds a caution. This "does not necessarily mean that ARIA introduced these errors", because those pages were also more complex. Even so, the pattern holds across the scan, and it is a good reason to reach for native HTML first.

Native elements already carry their roles and states. A real link, a button and a heading need no ARIA to work. As a result, a single-page application mostly needs ARIA in two places: the live region for status messages, and aria-current on the link for the current view. Angular sets that through ariaCurrentWhenActive on RouterLinkActive. Also, React Router's NavLink gives assistive technology context "when the link points to the current page."

Which framework pages should you build the fix from?#

Build from your own router's documentation first, then from the WCAG 2.2 Understanding pages that define what a correct result looks like. Each page below gives you one thing.

  1. Angular accessibility best practices: the NavigationEnd example that finds and focuses the main heading.
  2. Angular route definitions: the title property on each route and the TitleStrategy class.
  3. SvelteKit accessibility: the afterNavigate hook for moving focus somewhere other than the body.
  4. Next.js accessibility: how the built-in route announcer picks the text it reads.
  5. NuxtRouteAnnouncer: the component and its politeness prop.
  6. Vue accessibility guide: a watcher on the route that moves focus after each change.
  7. React Router accessibility: the list of jobs the app owns once the client scripts load.

Then read the Focus Order and Status Messages pages once, because they define what passing means.

What is the smallest route-change handler that works?#

In Angular, four lines on the router's NavigationEnd event find the new view's heading and focus it, and the route's title property names the page. The code below follows the Angular docs and adds a title to each route.

typescript
// app.routes.ts: one descriptive title per route
import {Routes} from '@angular/router';
import {Home} from './home';
import {Blog} from './home/blog';

export const routes: Routes = [
  {path: '', component: Home, title: 'Home'},
  {path: 'blog', component: Blog, title: 'Blog'},
];

// app.ts: focus the new view's heading after every navigation
import {Component, inject} from '@angular/core';
import {NavigationEnd, Router, RouterOutlet} from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: '<router-outlet />',
})
export class App {
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe((e) => {
      if (e instanceof NavigationEnd) {
        const mainHeader = document.querySelector<HTMLElement>('#main-content-header');
        mainHeader?.focus();
      }
    });
  }
}

// In each view's template: <h1 id="main-content-header" tabindex="-1">Blog</h1>

The steps are the same in any router. First, wait for the navigation to end. Then, find the new main heading. Finally, focus it, with tabindex="-1" on the heading so the call works. In SvelteKit, the same steps go inside afterNavigate from $app/navigation. In Vue, they go in a watcher on route.path, but the Vue guide moves focus to the page top instead.

How do you test a route change in five minutes?#

Navigate with the keyboard alone, press Tab once after each route change, then repeat with a screen reader and listen for the new page name. That short loop checks both jobs at once.

After one navigation, ask four questions. First, where is focus now, and can you see it? Second, does the next Tab reach something inside the new content? Third, does the screen reader say the new page name, once? Fourth, did the browser tab show a new, descriptive title?

  1. Where is focus now, and can you see it?

    Press nothing yet; look for the focus ring.

  2. Does the next Tab reach something inside the new content?

    Press Tab once.

  3. Does the screen reader say the new page name, once?

    Repeat the navigation with a screen reader on.

  4. Did the browser tab show a new, descriptive title?

    Check the tab, not only the heading.

A pass on all four meets the intent of Focus Order and Page Titled for that route. Then repeat for a route that loads data, and listen for the status message as well. Because many screen reader users move by headings first, press the heading key once and expect the new h1.

The screen reader compatibility testing guide turns this into a full plan across screen reader and browser pairs. Also, the accessibility testing guide shows where it fits in a wider test process.

When is client-side routing the wrong tool?#

When most of your views are documents rather than application state, plain page loads announce the title and reset focus with no router code at all. The SvelteKit docs describe both halves for server-rendered navigation, and the Next.js docs describe the title half.

Three cases call for a different choice. For example, a marketing site or a docs site is mostly reading. So plain links and server rendering give you both jobs for free. Next, a form flow of three or four steps works well as separate pages, since each step gets its own title. Finally, a dashboard with heavy state between views is where a client-side router earns its cost and the patch pays off.

Which kind of app needs a client-side router, and which gets both jobs from plain page loads. Source: SvelteKit and Next.js accessibility docs on server-rendered navigation, read October 2026.

Kind of appBetter choiceWhy
Marketing site or docs sitePlain links and server renderingMostly reading, so page loads do both jobs for free
Form flow of three or four stepsSeparate pagesEach step gets its own title
Dashboard with heavy state between viewsClient-side router plus the patchHere the router earns its cost

In short, if the app would work as plain pages, the most accessible single-page applications may be no single-page applications at all.

Three related guides go deeper on live regions, visible focus and screen reader testing, the three places a route-change fix usually needs more work. Start with the one that matches the job still weak.

If announcements are weak, the live regions guide explains polite and assertive regions and why a region must exist before its text. When focus is the problem, the focus and focus-visible guide shows how to keep a ring on a focused heading. And for testing, the screen reader guide above turns the four questions into a repeatable plan.

Some teams prefer outside help. Atyantik runs accessibility testing on web apps and supports WCAG and ADA compliance work. Still, the framework docs linked above hold everything needed to ship the fix yourself.

Questions this post answers

What do accessible single-page applications have to do on every route change?
Accessible single-page applications do two jobs on every client-side route change that a full page load used to do for free: announce the new page and move focus into it.
Where should focus go after a route change in a single-page application?
Move focus to the new view's h1 with tabindex set to -1, because user testing found a focused heading the clearest signal of a new page. Gatsby ran user tests with people with disabilities in July 2019.
Which routers announce a route change for you?
Next.js, SvelteKit and Nuxt can announce a route change for you, while React Router, Angular and Vue Router leave both the announcement and the focus move to your code. Next.js uses an assertive alert, while Nuxt defaults to polite.

Keep reading