Nuxt Route Announcements for Data Pages

Permalink to "Nuxt Route Announcements for Data Pages"

In a server-rendered site, every navigation is a page load: the screen reader announces the new title and starts reading. In a Nuxt app, client-side navigation swaps components without a page load, and screen reader users may hear nothing at all. Nuxt provides a route announcer component (NuxtRouteAnnouncer) that announces the page title on navigation. Data pages add a complication: filters, sorting and pagination often change the URL query, and announcing each of those as a “page change” would be wrong.

This guide belongs to Vue & Nuxt data components.

Spec reference

Permalink to "Spec reference"
  • SC 2.4.2 Page Titled (A) — each route has a descriptive title.
  • SC 4.1.3 Status Messages (AA) — announcing a new page title after client-side navigation is a status message technique.
  • SC 2.4.3 Focus Order (A) — after navigation, focus should be at a sensible starting point (often the main heading).
  • SC 3.2.2 On Input (A) — query changes from filters should not move focus or behave like navigation.
Navigation versus in-page update Flow: did the path change (a new report or record)? Treat it as navigation: route announcer plus focus on the heading. Did only the query change (filters, page, sort)? Treat it as an in-page update: keep focus and announce results with the shared announcer. Navigation versus in-page updatePath changed?/reports/revenue to /reports/costsnavigationNavigationtitle announced; focus to h1NuxtRouteAnnouncerOnly query changed??status=overdue&page=2in-page updateIn-page updatekeep focus; announce resultsshared announcerBack to a listrestore scroll and focussaved position

When to use — and when not to

Permalink to "When to use — and when not to"

Use the route announcer and heading focus for navigations between pages: from a list to a record, from one report to another. Use the data announcer for query-only changes driven by controls on the page.

Do not move focus to the heading when a user changes a filter that updates the query string; they would lose their place in the filter panel. The misapplication: a global router.afterEach that focuses h1 on every route change, including ?page=2.

Treating every URL change as navigation versus distinguishing them Comparison: focusing the heading on every URL change yanks users out of filter panels; distinguishing path from query changes keeps focus for in-page updates and moves it only on real navigation. Treating every URL change as navigation versus distinguishing them✗ Every URL change = navigationFilter change moves focus to h1Title announced for page 2Users lose place in controlsBack reloads at the top✓ Path versus queryFilter change keeps focus"Page 2 of 10" announcedUsers stay where they wereBack restores position

Annotated code example

Permalink to "Annotated code example"
<!-- app.vue -->
<template>
  <NuxtRouteAnnouncer />                                  <!-- announces page titles on navigation -->
  <NuxtLayout><NuxtPage /></NuxtLayout>
  <div role="status" class="visually-hidden">{{ polite }}</div>   <!-- data announcer -->
</template>
// plugins/route-focus.client.js — focus the heading only when the path changes
export default defineNuxtPlugin((nuxtApp) => {
  const router = useRouter();
  router.afterEach((to, from) => {
    if (to.path === from.path) return;                    // query-only: in-page update
    nextTick(() => {
      const h1 = document.querySelector('main h1');
      if (h1) { h1.setAttribute('tabindex', '-1'); h1.focus({ preventScroll: false }); }  // SC 2.4.3
    });
  });
});
// pages/invoices/index.vue — query-driven data, announced in place
const route = useRoute();
const { announce } = useAnnouncer();
const { data, pending } = await useFetch('/api/invoices', { query: computed(() => route.query) });
watch(data, (d) => d && announce(`${d.total} invoices, page ${route.query.page ?? 1} of ${d.pages}`, { key: 'results' }));
useHead({ title: computed(() => `Invoices${route.query.status ? ` — ${route.query.status}` : ''} — Acme`) });  // SC 2.4.2

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Action Expected result Expected announcement
Activate an invoice link Navigate to record; focus on h1 Page title, then heading
Change Status filter URL query updates; focus stays “42 invoices, page 1 of 2”
Next page Query updates; focus stays on pagination “Page 2 of 2”
Browser Back from a record List restored; focus on the row link Title of list page
Browser Back after a filter change Previous filter state Results message
A record visit and return Sequence among list page, router, record page and announcer: activating a row link navigates; the route announcer speaks the record title and focus moves to its heading; Back returns to the list, scroll and focus are restored to the row link. A record visit and returnList pageRouterRecord pageAnnounceractivate "INV-1042"render; focus h1"INV-1042 — Invoices — Acme"Backrestore scroll; focus rowlink
Navigations announce titles; returns restore position.

Integration context

Permalink to "Integration context"

Route-change focus for Vue Router without Nuxt is covered in managing route change focus in Angular and Vue. Returning to list positions is explained in restoring focus and scroll on browser back. The data announcer comes from a Vue announcer composable for data updates.

Gotchas

Permalink to "Gotchas"

Titles that never change. If every route has the same <title>, the route announcer repeats it. Set titles per page with useHead.

Double announcements. If you focus the h1 and the route announcer also speaks the title, users may hear the name twice. Make the title and heading complementary (“Invoice INV-1042 — Acme” and “INV-1042”), or rely on one mechanism.

Layouts with persistent filters. If the filter panel lives in a layout and the list in the page, path-based detection still works; make sure both use the same query state.

Common mistakes

Permalink to "Common mistakes"
  • No route announcer and no focus management.
  • Focusing h1 on query-only changes.
  • One title for every page.
  • Losing list position on Back.

Version and upgrade notes

Permalink to "Version and upgrade notes"

NuxtRouteAnnouncer was added in Nuxt 3.12; earlier versions need a custom announcer. Check its behaviour (politeness, title source) in your version and customise with its slot or props if needed.

Worked example

Permalink to "Worked example"

A Nuxt reporting app had no route announcements; screen reader users clicked report links and heard nothing, assuming the click had failed. Adding NuxtRouteAnnouncer fixed that — but a global focus plugin then yanked focus to the heading every time a filter changed. Limiting focus moves to path changes and announcing filter results with the data announcer gave the expected behaviour: navigations announce and move to the heading, filters keep focus and announce counts.

Writing titles for data pages

Permalink to "Writing titles for data pages"

Titles are what the route announcer speaks, so write them for listening: specific subject first, then context. “INV-1042 — Invoices — Acme” is better than “Acme | Invoices | Detail”. For filtered lists, include the main filter if users treat it as a distinct page (“Overdue invoices — Acme”), but not pagination or sort, which change too often to belong in a title. Keep titles under about 60 characters so they are quick to hear.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide a Nuxt layer or module with the route announcer, the path-versus-query focus plugin, the data announcer and a list-position restore helper, so every data page starts with correct behaviour.

FAQ

Permalink to "FAQ"
Does Nuxt announce route changes by default?

Recent Nuxt versions provide NuxtRouteAnnouncer; include it in app.vue.

Should focus move to the heading on every navigation?

On path changes, yes. On query-only changes from on-page controls, no.

What should the page title contain?

The page’s specific subject first, then the section and site: “INV-1042 — Invoices — Acme”.

How do I restore scroll and focus on Back?

Save the last focused row and scroll position per route, and restore after the list renders.

Can I use the route announcer for data updates?

Better not; keep route announcements for navigations and use the data announcer for results.

What about modals opened via routes?

Treat route-driven modals as dialogs: move focus into them and restore on close, without heading focus on the page behind.

Should route changes inside a dashboard tab set announce?

If tabs change the path, treat them as navigation with titles. If they only change the query, treat them as in-page updates and announce the tab’s content summary instead.

Permalink to "Related"

← Back to Vue & Nuxt Data Components