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.
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.
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 |
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.
Related
Permalink to "Related"- Vue announcer composable — data announcements
- Route change focus in Angular and Vue — Vue Router without Nuxt
- Restoring focus and scroll on Back — list positions