{
  "id": 9500220,
  "title": "Focus Management in the Next.js App Router: Field Notes on the Route Change That Loses Focus, inert, and Native dialog",
  "url": "https://urgent.news/2026/09/24/focus-management-in-the-next-js-app-router-field-notes-on-the-route",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-09-24T06:00:13.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/ahmed_mahmoud360/focus-management-in-the-nextjs-app-router-field-notes-on-the-route-change-that-loses-focus-295d"
  },
  "original_language": "en",
  "account": "The Next.js App Router introduces a client-side route announcer for screen readers, which reads the new document.title but does not handle focus changes. When navigating client-side, focus is left on the link that React just unmounted, causing the next Tab press to restart at the top of the document. To address this, a fifteen-line client component can be added to manage focus after route changes. This component uses usePathname() from next/navigation to detect changes and calls .focus({ preventScroll: true }) on a container with tabIndex={-1}, making it focusable via script while remaining invisible in the Tab order. The inert attribute, when added to a subtree, removes it from the Tab order, accessibility tree, and pointer hit testing. The native dialog element provides a focus-trapped modal experience when opened imperatively with showModal(), rendering the ::backdrop pseudo-element and making everything outside inert. However, rendering a dialog open in JSX results in a non-modal dialog without a focus trap or backdrop.",
  "summary": "The Next.js App Router announces client-side route changes to screen readers through its built-in route announcer, but it never moves keyboard focus. Focus is left on a link that React just unmounted, so the next Tab press restarts at the top of the document — and the fix is a fifteen-line client component plus one tabIndex={-1} . Key takeaways Next.js injects a route announcer on every…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}