next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Accessibility (a11y): next-route-announcer only announces title changes, H1/path fallbacks never used

Open Unit2795 opened this issue 2 months ago • 3 comments

Link to the code that reproduces this issue

https://github.com/Unit2795/next-announcer-issue

To Reproduce

  1. Ensure you have a screen reader and node.js installed.
  2. Clone the repository.
  3. Install dependencies using npm install.
  4. Run the development server using npm run dev.
  5. Start your screen reader
  6. Open the application in a web browser, you should see the homepage with a few link buttons.
  7. Click on the "Test" link to navigate to the /test page
  8. Observe that the screen reader announces the navigation change.
  9. Click on the "Test with Subpath" link to navigate to the /test/subpath page.
  10. Observe that the screen reader does not announce the navigation change.

Current vs. Expected behavior

Current:

  1. If there is a document title, check if it changed. If it didn't change, do not announce a change.
  2. Fallback to <h1> only occurs if there is no page title at all. (IE: document.title is undefined, null, or falsy.)
  3. Since pages usually have a title, the H1 fallback path is effectively a no-op.
  4. The path is never announced.

Expected:

  1. If document title changed, announcer reads new title. If unchanged, proceed to next step:
  2. If h1 changed, announcer reads new h1. If unchanged, proceed to next step:
  3. If URL path changed, announcer reads URL path

Provide environment information

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 11 Pro
  Available memory (MB): 65450
  Available CPU cores: 24
Binaries:
  Node: 25.2.1
  npm: 11.6.2
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.1.0-canary.4 // Latest available version is detected (16.1.0-canary.4).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Linking and Navigating

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

The next-route-announcer accessibility live region will generally only announce route changes when the document.title changes. It will not announce the title change only when there is no title at all (document.title is undefined, null, or falsy), but most sites generally have a title. So the logic for announcing based on an h1 or path is essentially a no op.

https://github.com/vercel/next.js/blob/737ddf775b491fb123fde41cc21a6a2d7ad599b9/packages/next/src/client/components/app-router-announcer.tsx#L48-L66

Based on the documentation (Next.js - Architecture: Accessibility), I would anticipate that the behavior should be cascading like I described in my expected behavior. What's more, the documentation for the app router mentions that the final fallback is to announce the path, but no such logic to announce the path exists for the app-router-announcer.

Sites should provide a unique title per page, according to WCAG SC 2.4.2. However, there could plausibly be minor state variations in the path. (IE: /product?sort=price vs /product?sort=rating OR /order/dinein vs /order/carryout). In this case, when the document title does not change, no announcement will be made by the next-route-announcer. This is not accessible; because a navigation has still occurred, UI may have changed slightly, focus may have shifted, and the user needs to be made aware of that.

Unit2795 avatar Nov 30 '25 08:11 Unit2795

Fix: Route Announcer Cascade Logic

Problem

The AppRouterAnnouncer component only announced route changes when document.title changed. This meant that:

  • When navigating between subpaths with the same title (e.g., /test/test/subpath), no announcement was made
  • The H1 fallback only worked if there was no title at all (which is rare in practice)
  • The path fallback was never used, even though it was documented as the final fallback

This created an accessibility issue where screen reader users weren't notified of route changes when only the path or H1 changed.

Solution

Implemented a cascade fallback logic that checks for changes in priority order:

  1. Title - If document.title changed → announce the new title
  2. H1 - If title didn't change but H1 changed → announce the new H1
  3. Path - If neither title nor H1 changed but path changed → announce the path

Changes Made

packages/next/src/client/components/app-router-announcer.tsx

  • Added import for extractPathFromFlightRouterState to get current path from router tree
  • Added refs to track previous values:
    • previousH1 - tracks previous H1 content
    • previousPath - tracks previous path
  • Implemented cascade logic in useEffect:
    • Checks title change first
    • Falls back to H1 if title unchanged
    • Falls back to path if both title and H1 unchanged
  • Updated comments to reflect new cascade behavior

test/unit/app-router-announcer.test.tsx (new file)

Created comprehensive unit tests covering:

  • First load (no announcement)
  • Title change announcement
  • H1 change announcement when title unchanged
  • Path change announcement when title and H1 unchanged
  • Priority: title over H1, H1 over path
  • Fallback scenarios (empty title, empty H1)
  • No announcement when nothing changes

How to Verify

Running Tests

npm test -- test/unit/app-router-announcer.test.tsx

All 9 tests should pass:

  • ✓ should not announce on first load
  • ✓ should announce when title changes
  • ✓ should announce H1 when title does not change but H1 changes
  • ✓ should announce path when title and H1 do not change but path changes
  • ✓ should prioritize title over H1 when both change
  • ✓ should prioritize H1 over path when title does not change
  • ✓ should handle empty title and use H1 fallback
  • ✓ should handle empty title and H1, use path fallback
  • ✓ should not announce when nothing changes

Manual Testing

  1. Test with screen reader:

    • Start a Next.js app with App Router
    • Navigate between routes with the same title but different paths
    • Verify that screen reader announces the route change (using H1 or path)
  2. Test cascade logic:

    • Navigate to a route with title "Test Page" and H1 "Test"
    • Navigate to /test/subpath with same title but different H1 "Subpath"
    • Screen reader should announce "Subpath" (H1 fallback)
    • Navigate to another route with same title and H1 but different path
    • Screen reader should announce the path
  3. Test priority:

    • Navigate between routes where both title and H1 change
    • Verify that title is announced (not H1)
    • Navigate where title stays same but H1 and path change
    • Verify that H1 is announced (not path)

Example Test Case

// Before: No announcement when navigating /test → /test/subpath
// (if title stays the same)

// After: Announces "Subpath" (H1) or "/test/subpath" (path)
// depending on what changed

Related Issue

Fixes #86660

Testing Environment

  • Jest with jsdom environment
  • React Testing Library
  • Shadow DOM support for announcer element

rosbitskyy avatar Nov 30 '25 10:11 rosbitskyy

@rosbitskyy Thank you for working on this fix, it looks great.

I have a few thoughts on the PR I saw:

  1. There appears to be another route announcer used in the pages router. I think this would need to be updated as it also does not implement the cascading logic. https://github.com/vercel/next.js/blob/canary/packages/next/src/client/route-announcer.tsx
  2. In your code, you check if the title, h1, and path all were undefined in order to not announce on first load. But only checking if the path was undefined should be enough?https://github.com/rosbitskyy/next.js/blob/6a88ec24ed4d5b511b839d925c3ed372f4a782bd/packages/next/src/client/components/app-router-announcer.tsx#L61-L63

More general questions about Next.js path change detection behavior:

  1. Does the URL path checking logic account for changes in query parameters? This is a tough one because query parameter changes could correlate with a navigation OR they are embedded by the current page to store data without navigation. Almost anything in the path could change but not actually correlate with navigation
  2. Related to the above point, does Next announce changes if a URL fragment (same-page link) is used?

General Thoughts

I know in my original message I mention that I expected cascading logic, just based on the language in the Next.js docs. I'm not sure if cascading logic for the route announcer is actually the best path forward though. Maybe a more thorough review on the navigation accessibility logic for Next.js might be warranted. Simply announcing based on path changes might result in announcements when it's not desired.

It might be worth allowing <Link> elements and useRouter to customize the behavior of the announcer. Like if links could turn off the announcer or override the message it would announce. Similarly, pages/layouts may wish to override the message announced based on dynamic data.

Unit2795 avatar Dec 06 '25 00:12 Unit2795

the PR I saw

fyi no PR has been filed in the next.js repo - the linked PR was opened (and merged) in a personal fork only.

stefanprobst avatar Dec 06 '25 06:12 stefanprobst