[{"data":1,"prerenderedAt":3162},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002F":314,"content-navigation":3010},[4,84,88,216],{"title":5,"path":6,"stem":7,"children":8},"Core Accessibility Principles For Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks","core-accessibility-principles-for-modern-frameworks",[9,12,18,24,36,48,66,78],{"title":10,"path":6,"stem":11},"Core Accessibility Principles for Modern Frameworks","core-accessibility-principles-for-modern-frameworks\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Accessible Color Contrast & Theming","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming","core-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002Findex",[17],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Accessible Form Validation & Error States","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states","core-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Focus Management Strategies for SPAs","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Findex",[29,30],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Handling Focus Restoration After Dynamic Route Changes","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Keyboard Navigation Patterns for Modals","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Findex",[41,42],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"Building Accessible Dropdowns Without External UI Kits","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits\u002Findex",[47],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Reduced Motion & Animation Accessibility","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Findex",[53,54,60],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Accessible Loading Skeletons and Spinners","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Respecting prefers-reduced-motion in React and CSS","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Screen Reader Compatibility Testing","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[71,72],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Testing ARIA Live Regions with Jest and Testing Library","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Semantic HTML vs ARIA in Component Trees","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees","core-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},"Modern Framework Accessibility","\u002F","index",{"title":89,"path":90,"stem":91,"children":92},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[93,96,108,132,156,162,180,204],{"title":94,"path":90,"stem":95},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":97,"path":98,"stem":99,"children":100},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[101,102],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Building Accessible Tabs in React Without Radix UI","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Accessible Data Tables & Grids in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Findex",[113,114,120,126],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Accessible Pagination for React Data Tables","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002Findex",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Building a Sortable Accessible Data Table in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"Virtualizing Long Lists Accessibly in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002Findex",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"Dynamic Content & State Announcements","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[137,138,144,150],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Accessible Toast Notifications in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Announcing Client-Side Route Changes in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"React Context for Accessibility Preferences","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences\u002Findex",[155],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Form Handling with React Hook Form & A11y","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Next.js App Router Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[167,168,174],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Skip Links in Next.js App Router","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Next.js Dynamic Imports & Keyboard Navigation","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002Findex",[179],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"React Hooks for Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[185,186,192,198],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Building a useAnnouncer Hook for Live Regions","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions\u002Findex",[191],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":196},"Fixing Focus Trap Issues in React Portals","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002Findex",[197],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":202},"Making React useEffect Accessible for Screen Readers","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers\u002Findex",[203],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":208},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[209,210],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":214},"Accessible Modals in Next.js 14 Server Components","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002Findex",[215],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":220},"Testing And Automating Accessibility","\u002Ftesting-and-automating-accessibility","testing-and-automating-accessibility",[221,224,242,260,278,296],{"title":222,"path":218,"stem":223},"Testing & Automating Accessibility","testing-and-automating-accessibility\u002Findex",{"title":225,"path":226,"stem":227,"children":228},"Accessibility Audits with Lighthouse","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Findex",[229,230,236],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"Interpreting Lighthouse Accessibility Scores","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores\u002Findex",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"Setting Lighthouse CI Accessibility Budgets","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets\u002Findex",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Automated Accessibility Testing with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Findex",[247,248,254],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Catching Color Contrast Failures with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Writing Custom axe-core Rules","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002Findex",[259],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"Component Testing with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Findex",[265,266,272],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Debugging jest-axe Violations in CI","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"Testing React Components with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe\u002Findex",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"End-to-End Accessibility Testing with Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Findex",[283,284,290],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Asserting Focus Order in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright\u002Findex",[289],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"Keyboard Navigation Tests in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright\u002Findex",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Gating Accessibility in CI\u002FCD Pipelines","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Findex",[301,302,308],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"Accessibility Regression Testing in GitHub Actions","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions\u002Findex",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Failing Pull Requests on axe Violations","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations\u002Findex",[313],{"title":309,"path":310,"stem":311},{"id":315,"title":25,"body":316,"date":3003,"description":3004,"extension":3005,"image":3003,"meta":3006,"modifiedAt":3003,"navigation":686,"noindex":3007,"path":26,"publishedAt":3003,"seo":3008,"stem":27,"updatedAt":3003,"__hash__":3009},"content\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Findex.md",{"type":317,"value":318,"toc":2987},"minimark",[319,323,332,338,363,368,382,387,407,549,556,560,563,574,594,608,612,615,620,634,875,901,905,919,1106,1135,1139,1146,1507,1530,1539,1543,1552,1563,1910,1924,2435,2463,2480,2485,2489,2496,2507,2510,2528,2537,2541,2551,2760,2774,2778,2839,2843,2846,2879,2883,2889,2902,2908,2926,2940,2959,2963,2983],[320,321,25],"h1",{"id":322},"focus-management-strategies-for-spas",[324,325,326,327,331],"p",{},"Single-page applications (SPAs) fundamentally alter how browsers handle navigation and DOM updates, often breaking default keyboard navigation flows. Effective focus management requires developers to programmatically control the keyboard cursor during route transitions, modal interactions, and dynamic content injections. This guide bridges foundational concepts from ",[328,329,10],"a",{"href":330},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002F"," with framework-specific routing hooks, ensuring users relying on assistive technologies maintain context without losing their place in the application.",[324,333,334],{},[335,336,337],"strong",{},"WCAG Success Criteria Mapped:",[339,340,341,348,353,358],"ul",{},[342,343,344],"li",{},[345,346,347],"code",{},"2.1.1 Keyboard",[342,349,350],{},[345,351,352],{},"2.4.3 Focus Order",[342,354,355],{},[345,356,357],{},"2.4.7 Focus Visible",[342,359,360],{},[345,361,362],{},"4.1.2 Name, Role, Value",[324,364,365],{},[335,366,367],{},"Key Implementation Objectives:",[339,369,370,373,376,379],{},[342,371,372],{},"Understand how virtual DOM diffing disrupts native tab order",[342,374,375],{},"Implement programmatic focus routing on navigation events",[342,377,378],{},"Apply focus trapping for overlays and interactive widgets",[342,380,381],{},"Validate focus behavior across framework lifecycle hooks",[383,384,386],"h2",{"id":385},"the-focus-lifecycle-in-a-single-page-application","The Focus Lifecycle in a Single-Page Application",[324,388,389,390,394,395,398,399,402,403,406],{},"Before writing any code, it helps to have a mental model of where focus ",[391,392,393],"em",{},"should"," travel during the two events that matter most in an SPA: opening a transient overlay and navigating between routes. The diagram below traces both journeys. On the overlay path, focus moves from the trigger into the trapped dialog, cycles within it, and—critically—returns to the originating trigger on ",[345,396,397],{},"Escape"," (",[345,400,401],{},"2.4.3","). On the route path, a client-side navigation leaves focus on a stale node, so we explicitly relocate it to the new view's main heading where it remains visible (",[345,404,405],{},"2.4.7",").",[408,409,416,417,416,421,416,425,416,475,416,517],"svg",{"role":410,"ariaLabelledBy":411,"viewBox":414,"style":415},"img",[412,413],"flc-t","flc-d","0 0 760 360","width:100%;height:auto;max-width:760px","\n  ",[418,419,420],"title",{"id":412},"Focus lifecycle in a single-page application",[422,423,424],"desc",{"id":413},"Two flows. Top: a trigger button opens an overlay where focus is trapped and cycles, then Escape restores focus to the trigger, satisfying focus order. Bottom: a route change leaves focus stranded, so focus is moved to the new view main heading and stays visible.",[426,427,432,433,432,443,432,448,432,451,432,454,432,456,432,458,432,462,432,465,432,469,432,472,416],"g",{"style":428,"fill":429,"stroke":430,"color":431},"stroke-width:2","none","currentColor","var(--primary)","\n    ",[434,435],"rect",{"x":436,"y":437,"width":438,"height":439,"rx":440,"fill":441,"stroke":442},"20","40","150","56","8","var(--surface)","var(--border)",[434,444],{"x":445,"y":437,"width":446,"height":439,"rx":440,"fill":447},"250","170","var(--primary-soft)",[434,449],{"x":450,"y":437,"width":438,"height":439,"rx":440,"fill":441,"stroke":442},"500",[434,452],{"x":436,"y":453,"width":438,"height":439,"rx":440,"fill":441,"stroke":442},"220",[434,455],{"x":445,"y":453,"width":446,"height":439,"rx":440,"fill":447},[434,457],{"x":450,"y":453,"width":438,"height":439,"rx":440,"fill":447},[459,460],"path",{"d":461},"M170 68 L246 68",[459,463],{"d":464},"M420 68 L496 68",[459,466],{"style":467,"d":468},"stroke-dasharray:6 5","M420 88 q40 60 -250 0",[459,470],{"d":471},"M170 248 L246 248",[459,473],{"d":474},"M420 248 L496 248",[426,476,432,479,432,485,432,490,432,496,432,499,432,503,432,507,432,511,432,514,416],{"style":477,"fill":478},"font-size:14px;text-anchor:middle","var(--text)",[480,481,484],"text",{"x":482,"y":483},"95","73","Trigger button",[480,486,489],{"x":487,"y":488},"335","65","Overlay opens",[480,491,495],{"style":492,"x":487,"y":493,"fill":494},"font-size:12px","83","var(--muted)","focus trapped, cycles",[480,497,397],{"x":498,"y":483},"575",[480,500,502],{"x":482,"y":501},"253","Route change",[480,504,506],{"x":487,"y":505},"245","Focus stranded",[480,508,510],{"style":492,"x":487,"y":509,"fill":494},"263","on detached node",[480,512,513],{"x":498,"y":505},"Move to main",[480,515,516],{"style":492,"x":498,"y":509,"fill":494},"heading, visible",[426,518,432,520,432,525,432,529,432,533,432,537,432,540,432,546,416],{"style":519,"fill":494},"font-size:12px;text-anchor:middle",[480,521,524],{"x":522,"y":523},"208","58","opens",[480,526,528],{"x":527,"y":523},"458","dismiss",[480,530,532],{"x":487,"y":531},"148","restore focus to trigger (2.4.3)",[480,534,536],{"x":522,"y":535},"238","leaves",[480,538,539],{"x":527,"y":535},"relocate (2.4.7)",[480,541,545],{"style":542,"x":543,"y":544},"font-weight:bold","380","180","Route navigation flow",[480,547,548],{"style":542,"x":543,"y":436},"Overlay focus flow",[324,550,551,552,555],{},"Keeping this lifecycle in view clarifies why a single, centralized focus manager outperforms ad-hoc ",[345,553,554],{},"focus()"," calls sprinkled across components: each arrow in the diagram is an explicit decision your application must own, because the browser no longer makes it for you.",[383,557,559],{"id":558},"understanding-spa-navigation-focus-disruption","Understanding SPA Navigation & Focus Disruption",[324,561,562],{},"Client-side routing bypasses full page reloads, leaving focus stranded on stale DOM nodes that may have been detached during virtual DOM reconciliation. When a router swaps components, the browser does not automatically reset the accessibility tree. Without explicit intervention, the keyboard cursor remains on the previously focused element, which may now be hidden or destroyed, causing severe disorientation for keyboard and screen reader users.",[324,564,565,566,569,570,573],{},"In a traditional multi-page application, every navigation triggers a document load. The browser resets focus to the top of the document, the screen reader announces the new page title, and the tab order is rebuilt from scratch. SPAs discard this contract entirely. A ",[345,567,568],{},"pushState"," call updates the URL and swaps a subtree of the DOM, but as far as the browser is concerned nothing navigation-worthy happened. The accessibility tree is patched in place, focus stays wherever it was, and—if the previously focused element was removed—focus silently falls back to ",[345,571,572],{},"document.body",", where keyboard users find themselves at the very top of the tab order with no announcement.",[324,575,576,577,580,581,584,585,588,589,593],{},"Establishing a centralized focus manager pattern prevents scattered, component-level implementations that conflict during concurrent updates. A good manager exposes a small, intentional API: ",[345,578,579],{},"moveFocusToView()"," for navigations, ",[345,582,583],{},"trap(container)"," for overlays, and ",[345,586,587],{},"restore()"," for returning focus to a stored trigger. Centralizing these decisions also makes the behavior testable in isolation and prevents the race conditions that emerge when two components both try to claim focus during the same render. For detailed patterns on synchronizing focus with asynchronous route transitions, consult the guide on ",[328,590,592],{"href":591},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes\u002F","Handling focus restoration after dynamic route changes",".",[324,595,596,599,600,603,604,607],{},[335,597,598],{},"Testing Hook:"," Verify that pressing ",[345,601,602],{},"Tab"," immediately after a route change moves focus to the main content heading (",[345,605,606],{},"\u003Ch1>",") or first interactive element, not the browser URL bar or detached DOM nodes.",[383,609,611],{"id":610},"framework-specific-focus-routing-hooks","Framework-Specific Focus Routing Hooks",[324,613,614],{},"Intercepting navigation events requires leveraging framework-specific lifecycle methods to ensure focus shifts occur after the DOM has stabilized. Below are production-ready implementations addressing hydration, reactivity, and cleanup.",[616,617,619],"h3",{"id":618},"react-functional-components","React (Functional Components)",[324,621,622,623,625,626,629,630,633],{},"React's hydration phase can cause focus mismatches if ",[345,624,554],{}," is called during server-side rendering or before the client mounts. Use ",[345,627,628],{},"useLayoutEffect"," to guarantee synchronous execution after DOM mutations, and always target a container with ",[345,631,632],{},"tabindex=\"-1\""," to avoid altering the natural tab order.",[635,636,641],"pre",{"className":637,"code":638,"language":639,"meta":640,"style":640},"language-jsx shiki shiki-themes github-light github-dark","import { useLayoutEffect, useRef } from 'react';\nimport { useLocation } from 'react-router-dom';\n\nexport function FocusManager() {\n  const { pathname } = useLocation();\n  const mainRef = useRef(null);\n\n  useLayoutEffect(() => {\n    if (mainRef.current) {\n      mainRef.current.setAttribute('tabindex', '-1');\n      mainRef.current.focus({ preventScroll: true });\n    }\n  }, [pathname]);\n\n  return \u003Cmain ref={mainRef} id=\"main-content\" \u002F>;\n}\n","jsx","",[345,642,643,666,681,688,704,729,752,757,772,781,803,820,826,832,837,869],{"__ignoreMap":640},[644,645,648,652,656,659,663],"span",{"class":646,"line":647},"line",1,[644,649,651],{"class":650},"szBVR","import",[644,653,655],{"class":654},"sVt8B"," { useLayoutEffect, useRef } ",[644,657,658],{"class":650},"from",[644,660,662],{"class":661},"sZZnC"," 'react'",[644,664,665],{"class":654},";\n",[644,667,669,671,674,676,679],{"class":646,"line":668},2,[644,670,651],{"class":650},[644,672,673],{"class":654}," { useLocation } ",[644,675,658],{"class":650},[644,677,678],{"class":661}," 'react-router-dom'",[644,680,665],{"class":654},[644,682,684],{"class":646,"line":683},3,[644,685,687],{"emptyLinePlaceholder":686},true,"\n",[644,689,691,694,697,701],{"class":646,"line":690},4,[644,692,693],{"class":650},"export",[644,695,696],{"class":650}," function",[644,698,700],{"class":699},"sScJk"," FocusManager",[644,702,703],{"class":654},"() {\n",[644,705,707,710,713,717,720,723,726],{"class":646,"line":706},5,[644,708,709],{"class":650},"  const",[644,711,712],{"class":654}," { ",[644,714,716],{"class":715},"sj4cs","pathname",[644,718,719],{"class":654}," } ",[644,721,722],{"class":650},"=",[644,724,725],{"class":699}," useLocation",[644,727,728],{"class":654},"();\n",[644,730,732,734,737,740,743,746,749],{"class":646,"line":731},6,[644,733,709],{"class":650},[644,735,736],{"class":715}," mainRef",[644,738,739],{"class":650}," =",[644,741,742],{"class":699}," useRef",[644,744,745],{"class":654},"(",[644,747,748],{"class":715},"null",[644,750,751],{"class":654},");\n",[644,753,755],{"class":646,"line":754},7,[644,756,687],{"emptyLinePlaceholder":686},[644,758,760,763,766,769],{"class":646,"line":759},8,[644,761,762],{"class":699},"  useLayoutEffect",[644,764,765],{"class":654},"(() ",[644,767,768],{"class":650},"=>",[644,770,771],{"class":654}," {\n",[644,773,775,778],{"class":646,"line":774},9,[644,776,777],{"class":650},"    if",[644,779,780],{"class":654}," (mainRef.current) {\n",[644,782,784,787,790,792,795,798,801],{"class":646,"line":783},10,[644,785,786],{"class":654},"      mainRef.current.",[644,788,789],{"class":699},"setAttribute",[644,791,745],{"class":654},[644,793,794],{"class":661},"'tabindex'",[644,796,797],{"class":654},", ",[644,799,800],{"class":661},"'-1'",[644,802,751],{"class":654},[644,804,806,808,811,814,817],{"class":646,"line":805},11,[644,807,786],{"class":654},[644,809,810],{"class":699},"focus",[644,812,813],{"class":654},"({ preventScroll: ",[644,815,816],{"class":715},"true",[644,818,819],{"class":654}," });\n",[644,821,823],{"class":646,"line":822},12,[644,824,825],{"class":654},"    }\n",[644,827,829],{"class":646,"line":828},13,[644,830,831],{"class":654},"  }, [pathname]);\n",[644,833,835],{"class":646,"line":834},14,[644,836,687],{"emptyLinePlaceholder":686},[644,838,840,843,846,850,853,855,858,861,863,866],{"class":646,"line":839},15,[644,841,842],{"class":650},"  return",[644,844,845],{"class":654}," \u003C",[644,847,849],{"class":848},"s9eBZ","main",[644,851,852],{"class":699}," ref",[644,854,722],{"class":650},[644,856,857],{"class":654},"{mainRef} ",[644,859,860],{"class":699},"id",[644,862,722],{"class":650},[644,864,865],{"class":661},"\"main-content\"",[644,867,868],{"class":654}," \u002F>;\n",[644,870,872],{"class":646,"line":871},16,[644,873,874],{"class":654},"}\n",[324,876,877,878,881,882,884,885,888,889,892,893,896,897,900],{},"One subtlety worth calling out: in the React Router data APIs (",[345,879,880],{},"createBrowserRouter","), navigations can resolve loaders asynchronously, so ",[345,883,716],{}," may update before the new view has actually painted. When you adopt ",[345,886,887],{},"\u003CRouterProvider>",", prefer the framework's built-in ",[345,890,891],{},"\u003CScrollRestoration>"," for scroll and pair it with a focus effect keyed on ",[345,894,895],{},"useNavigation().state === 'idle'"," so focus only moves once the transition completes. This avoids the classic bug where focus snaps to the ",[391,898,899],{},"previous"," view because the effect fired mid-transition.",[616,902,904],{"id":903},"vue-3-composition-api","Vue 3 (Composition API)",[324,906,907,908,911,912,915,916,918],{},"Vue's reactivity system batches DOM updates. Using ",[345,909,910],{},"nextTick"," ensures the router has flushed pending updates before querying the DOM. This prevents ",[345,913,914],{},"querySelector"," from returning ",[345,917,748],{}," during rapid navigation or concurrent component patches.",[635,920,924],{"className":921,"code":922,"language":923,"meta":640,"style":640},"language-javascript shiki shiki-themes github-light github-dark","import { watch, nextTick } from 'vue';\nimport { useRoute } from 'vue-router';\n\nexport function useRouteFocus() {\n  const route = useRoute();\n\n  watch(\n    () => route.path,\n    async () => {\n      await nextTick();\n      const target = document.querySelector('[data-focus-target]');\n      if (target) {\n        target.setAttribute('tabindex', '-1');\n        target.focus({ preventScroll: true });\n      }\n    }\n  );\n}\n","javascript",[345,925,926,940,954,958,969,983,987,995,1005,1017,1027,1049,1057,1074,1086,1091,1095,1101],{"__ignoreMap":640},[644,927,928,930,933,935,938],{"class":646,"line":647},[644,929,651],{"class":650},[644,931,932],{"class":654}," { watch, nextTick } ",[644,934,658],{"class":650},[644,936,937],{"class":661}," 'vue'",[644,939,665],{"class":654},[644,941,942,944,947,949,952],{"class":646,"line":668},[644,943,651],{"class":650},[644,945,946],{"class":654}," { useRoute } ",[644,948,658],{"class":650},[644,950,951],{"class":661}," 'vue-router'",[644,953,665],{"class":654},[644,955,956],{"class":646,"line":683},[644,957,687],{"emptyLinePlaceholder":686},[644,959,960,962,964,967],{"class":646,"line":690},[644,961,693],{"class":650},[644,963,696],{"class":650},[644,965,966],{"class":699}," useRouteFocus",[644,968,703],{"class":654},[644,970,971,973,976,978,981],{"class":646,"line":706},[644,972,709],{"class":650},[644,974,975],{"class":715}," route",[644,977,739],{"class":650},[644,979,980],{"class":699}," useRoute",[644,982,728],{"class":654},[644,984,985],{"class":646,"line":731},[644,986,687],{"emptyLinePlaceholder":686},[644,988,989,992],{"class":646,"line":754},[644,990,991],{"class":699},"  watch",[644,993,994],{"class":654},"(\n",[644,996,997,1000,1002],{"class":646,"line":759},[644,998,999],{"class":654},"    () ",[644,1001,768],{"class":650},[644,1003,1004],{"class":654}," route.path,\n",[644,1006,1007,1010,1013,1015],{"class":646,"line":774},[644,1008,1009],{"class":650},"    async",[644,1011,1012],{"class":654}," () ",[644,1014,768],{"class":650},[644,1016,771],{"class":654},[644,1018,1019,1022,1025],{"class":646,"line":783},[644,1020,1021],{"class":650},"      await",[644,1023,1024],{"class":699}," nextTick",[644,1026,728],{"class":654},[644,1028,1029,1032,1035,1037,1040,1042,1044,1047],{"class":646,"line":805},[644,1030,1031],{"class":650},"      const",[644,1033,1034],{"class":715}," target",[644,1036,739],{"class":650},[644,1038,1039],{"class":654}," document.",[644,1041,914],{"class":699},[644,1043,745],{"class":654},[644,1045,1046],{"class":661},"'[data-focus-target]'",[644,1048,751],{"class":654},[644,1050,1051,1054],{"class":646,"line":822},[644,1052,1053],{"class":650},"      if",[644,1055,1056],{"class":654}," (target) {\n",[644,1058,1059,1062,1064,1066,1068,1070,1072],{"class":646,"line":828},[644,1060,1061],{"class":654},"        target.",[644,1063,789],{"class":699},[644,1065,745],{"class":654},[644,1067,794],{"class":661},[644,1069,797],{"class":654},[644,1071,800],{"class":661},[644,1073,751],{"class":654},[644,1075,1076,1078,1080,1082,1084],{"class":646,"line":834},[644,1077,1061],{"class":654},[644,1079,810],{"class":699},[644,1081,813],{"class":654},[644,1083,816],{"class":715},[644,1085,819],{"class":654},[644,1087,1088],{"class":646,"line":839},[644,1089,1090],{"class":654},"      }\n",[644,1092,1093],{"class":646,"line":871},[644,1094,825],{"class":654},[644,1096,1098],{"class":646,"line":1097},17,[644,1099,1100],{"class":654},"  );\n",[644,1102,1104],{"class":646,"line":1103},18,[644,1105,874],{"class":654},[324,1107,1108,1109,1112,1113,1116,1117,1120,1121,1124,1125,1127,1128,1130,1131,1134],{},"In Nuxt specifically, watch out for the hydration boundary: registering this watcher inside a ",[345,1110,1111],{},"\u003CClientOnly>"," boundary or guarding it with ",[345,1114,1115],{},"import.meta.client"," prevents the focus call from running during server rendering where ",[345,1118,1119],{},"document"," is undefined. Nuxt's ",[345,1122,1123],{},"\u003CNuxtPage>"," transitions also fire after ",[345,1126,910],{},", so a single ",[345,1129,910],{}," is usually sufficient; if you use page transitions with a delay, await the transition's ",[345,1132,1133],{},"onAfterEnter"," hook instead.",[616,1136,1138],{"id":1137},"angular-router-integration","Angular (Router Integration)",[324,1140,1141,1142,1145],{},"Angular's change detection cycle requires subscribing to ",[345,1143,1144],{},"Router.events"," and manually unsubscribing to prevent memory leaks. Use a dedicated service to handle focus shifts safely across component lifecycles.",[635,1147,1151],{"className":1148,"code":1149,"language":1150,"meta":640,"style":640},"language-typescript shiki shiki-themes github-light github-dark","import { Injectable, OnDestroy } from '@angular\u002Fcore';\nimport { Router, NavigationEnd } from '@angular\u002Frouter';\nimport { filter, takeUntil } from 'rxjs\u002Foperators';\nimport { Subject } from 'rxjs';\n\n@Injectable({ providedIn: 'root' })\nexport class FocusRoutingService implements OnDestroy {\n  private destroy$ = new Subject\u003Cvoid>();\n\n  constructor(private router: Router) {\n    this.router.events.pipe(\n      filter(event => event instanceof NavigationEnd),\n      takeUntil(this.destroy$)\n    ).subscribe(() => {\n      const target = document.querySelector('main') || document.querySelector('h1');\n      if (target) {\n        target.setAttribute('tabindex', '-1');\n        (target as HTMLElement).focus({ preventScroll: true });\n      }\n    });\n  }\n\n  ngOnDestroy() {\n    this.destroy$.next();\n    this.destroy$.complete();\n  }\n}\n","typescript",[345,1152,1153,1167,1181,1195,1209,1213,1230,1248,1274,1278,1300,1313,1338,1351,1365,1399,1405,1421,1442,1447,1453,1459,1464,1472,1485,1497,1502],{"__ignoreMap":640},[644,1154,1155,1157,1160,1162,1165],{"class":646,"line":647},[644,1156,651],{"class":650},[644,1158,1159],{"class":654}," { Injectable, OnDestroy } ",[644,1161,658],{"class":650},[644,1163,1164],{"class":661}," '@angular\u002Fcore'",[644,1166,665],{"class":654},[644,1168,1169,1171,1174,1176,1179],{"class":646,"line":668},[644,1170,651],{"class":650},[644,1172,1173],{"class":654}," { Router, NavigationEnd } ",[644,1175,658],{"class":650},[644,1177,1178],{"class":661}," '@angular\u002Frouter'",[644,1180,665],{"class":654},[644,1182,1183,1185,1188,1190,1193],{"class":646,"line":683},[644,1184,651],{"class":650},[644,1186,1187],{"class":654}," { filter, takeUntil } ",[644,1189,658],{"class":650},[644,1191,1192],{"class":661}," 'rxjs\u002Foperators'",[644,1194,665],{"class":654},[644,1196,1197,1199,1202,1204,1207],{"class":646,"line":690},[644,1198,651],{"class":650},[644,1200,1201],{"class":654}," { Subject } ",[644,1203,658],{"class":650},[644,1205,1206],{"class":661}," 'rxjs'",[644,1208,665],{"class":654},[644,1210,1211],{"class":646,"line":706},[644,1212,687],{"emptyLinePlaceholder":686},[644,1214,1215,1218,1221,1224,1227],{"class":646,"line":731},[644,1216,1217],{"class":654},"@",[644,1219,1220],{"class":699},"Injectable",[644,1222,1223],{"class":654},"({ providedIn: ",[644,1225,1226],{"class":661},"'root'",[644,1228,1229],{"class":654}," })\n",[644,1231,1232,1234,1237,1240,1243,1246],{"class":646,"line":754},[644,1233,693],{"class":650},[644,1235,1236],{"class":650}," class",[644,1238,1239],{"class":699}," FocusRoutingService",[644,1241,1242],{"class":650}," implements",[644,1244,1245],{"class":699}," OnDestroy",[644,1247,771],{"class":654},[644,1249,1250,1253,1257,1259,1262,1265,1268,1271],{"class":646,"line":759},[644,1251,1252],{"class":650},"  private",[644,1254,1256],{"class":1255},"s4XuR"," destroy$",[644,1258,739],{"class":650},[644,1260,1261],{"class":650}," new",[644,1263,1264],{"class":699}," Subject",[644,1266,1267],{"class":654},"\u003C",[644,1269,1270],{"class":715},"void",[644,1272,1273],{"class":654},">();\n",[644,1275,1276],{"class":646,"line":774},[644,1277,687],{"emptyLinePlaceholder":686},[644,1279,1280,1283,1285,1288,1291,1294,1297],{"class":646,"line":783},[644,1281,1282],{"class":650},"  constructor",[644,1284,745],{"class":654},[644,1286,1287],{"class":650},"private",[644,1289,1290],{"class":1255}," router",[644,1292,1293],{"class":650},":",[644,1295,1296],{"class":699}," Router",[644,1298,1299],{"class":654},") {\n",[644,1301,1302,1305,1308,1311],{"class":646,"line":805},[644,1303,1304],{"class":715},"    this",[644,1306,1307],{"class":654},".router.events.",[644,1309,1310],{"class":699},"pipe",[644,1312,994],{"class":654},[644,1314,1315,1318,1320,1323,1326,1329,1332,1335],{"class":646,"line":822},[644,1316,1317],{"class":699},"      filter",[644,1319,745],{"class":654},[644,1321,1322],{"class":1255},"event",[644,1324,1325],{"class":650}," =>",[644,1327,1328],{"class":654}," event ",[644,1330,1331],{"class":650},"instanceof",[644,1333,1334],{"class":699}," NavigationEnd",[644,1336,1337],{"class":654},"),\n",[644,1339,1340,1343,1345,1348],{"class":646,"line":828},[644,1341,1342],{"class":699},"      takeUntil",[644,1344,745],{"class":654},[644,1346,1347],{"class":715},"this",[644,1349,1350],{"class":654},".destroy$)\n",[644,1352,1353,1356,1359,1361,1363],{"class":646,"line":834},[644,1354,1355],{"class":654},"    ).",[644,1357,1358],{"class":699},"subscribe",[644,1360,765],{"class":654},[644,1362,768],{"class":650},[644,1364,771],{"class":654},[644,1366,1367,1369,1371,1373,1375,1377,1379,1382,1385,1388,1390,1392,1394,1397],{"class":646,"line":839},[644,1368,1031],{"class":650},[644,1370,1034],{"class":715},[644,1372,739],{"class":650},[644,1374,1039],{"class":654},[644,1376,914],{"class":699},[644,1378,745],{"class":654},[644,1380,1381],{"class":661},"'main'",[644,1383,1384],{"class":654},") ",[644,1386,1387],{"class":650},"||",[644,1389,1039],{"class":654},[644,1391,914],{"class":699},[644,1393,745],{"class":654},[644,1395,1396],{"class":661},"'h1'",[644,1398,751],{"class":654},[644,1400,1401,1403],{"class":646,"line":871},[644,1402,1053],{"class":650},[644,1404,1056],{"class":654},[644,1406,1407,1409,1411,1413,1415,1417,1419],{"class":646,"line":1097},[644,1408,1061],{"class":654},[644,1410,789],{"class":699},[644,1412,745],{"class":654},[644,1414,794],{"class":661},[644,1416,797],{"class":654},[644,1418,800],{"class":661},[644,1420,751],{"class":654},[644,1422,1423,1426,1429,1432,1434,1436,1438,1440],{"class":646,"line":1103},[644,1424,1425],{"class":654},"        (target ",[644,1427,1428],{"class":650},"as",[644,1430,1431],{"class":699}," HTMLElement",[644,1433,406],{"class":654},[644,1435,810],{"class":699},[644,1437,813],{"class":654},[644,1439,816],{"class":715},[644,1441,819],{"class":654},[644,1443,1445],{"class":646,"line":1444},19,[644,1446,1090],{"class":654},[644,1448,1450],{"class":646,"line":1449},20,[644,1451,1452],{"class":654},"    });\n",[644,1454,1456],{"class":646,"line":1455},21,[644,1457,1458],{"class":654},"  }\n",[644,1460,1462],{"class":646,"line":1461},22,[644,1463,687],{"emptyLinePlaceholder":686},[644,1465,1467,1470],{"class":646,"line":1466},23,[644,1468,1469],{"class":699},"  ngOnDestroy",[644,1471,703],{"class":654},[644,1473,1475,1477,1480,1483],{"class":646,"line":1474},24,[644,1476,1304],{"class":715},[644,1478,1479],{"class":654},".destroy$.",[644,1481,1482],{"class":699},"next",[644,1484,728],{"class":654},[644,1486,1488,1490,1492,1495],{"class":646,"line":1487},25,[644,1489,1304],{"class":715},[644,1491,1479],{"class":654},[644,1493,1494],{"class":699},"complete",[644,1496,728],{"class":654},[644,1498,1500],{"class":646,"line":1499},26,[644,1501,1458],{"class":654},[644,1503,1505],{"class":646,"line":1504},27,[644,1506,874],{"class":654},[324,1508,1509,1510,1513,1514,1517,1518,1521,1522,1525,1526,1529],{},"Because this service is ",[345,1511,1512],{},"providedIn: 'root'"," it lives for the application's lifetime, so the ",[345,1515,1516],{},"destroy$"," teardown is defensive rather than strictly required—but keeping it makes the pattern copy-safe for component-scoped variants. Angular also ships a ",[345,1519,1520],{},"LiveAnnouncer"," in ",[345,1523,1524],{},"@angular\u002Fcdk\u002Fa11y"," and a ",[345,1527,1528],{},"FocusTrap"," factory; reaching for the CDK before hand-rolling traps saves you from re-implementing edge cases the Material team has already hardened.",[324,1531,1532,1534,1535,1538],{},[335,1533,598],{}," Use browser DevTools (",[345,1536,1537],{},"document.activeElement",") immediately after route transitions to confirm the target element matches your accessibility audit expectations.",[383,1540,1542],{"id":1541},"focus-trapping-for-modals-overlays","Focus Trapping for Modals & Overlays",[324,1544,1545,1546,86,1548,1551],{},"When rendering overlays, cyclic focus containment prevents users from tabbing behind the modal into the background application. The trap must calculate the first and last focusable nodes, intercept ",[345,1547,602],{},[345,1549,1550],{},"Shift+Tab"," events, and loop boundaries. Crucially, when overlays are rendered via portals, the trap must query the portal's container node, not the parent component tree, to avoid missing dynamically injected elements.",[324,1553,1554,1555,1558,1559,1562],{},"Focus restoration to the triggering element upon dismissal is non-negotiable for WCAG compliance. While ARIA roles (",[345,1556,1557],{},"role=\"dialog\"",") are necessary, they should complement, not replace, proper DOM structure. Refer to ",[328,1560,79],{"href":1561},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002F"," for architectural guidance on balancing native semantics with programmatic overrides.",[635,1564,1566],{"className":921,"code":1565,"language":923,"meta":640,"style":640},"\u002F**\n * Framework-agnostic focus trap utility.\n * Attach to the overlay container after it mounts.\n * Handles portal boundaries by querying the passed container reference.\n *\u002F\nexport function createFocusTrap(container) {\n  const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n  const focusable = Array.from(container.querySelectorAll(focusableSelectors));\n  if (focusable.length === 0) return () => {};\n\n  const first = focusable[0];\n  const last = focusable[focusable.length - 1];\n\n  const handleTab = (e) => {\n    if (e.key !== 'Tab') return;\n    if (e.shiftKey && document.activeElement === first) {\n      e.preventDefault();\n      last.focus();\n    } else if (!e.shiftKey && document.activeElement === last) {\n      e.preventDefault();\n      first.focus();\n    }\n  };\n\n  container.addEventListener('keydown', handleTab);\n\n  \u002F\u002F Return cleanup function for framework unmount\n  return () => container.removeEventListener('keydown', handleTab);\n}\n",[345,1567,1568,1574,1579,1584,1589,1594,1610,1624,1647,1676,1680,1698,1720,1724,1744,1763,1782,1792,1801,1829,1837,1846,1850,1855,1859,1875,1879,1884,1905],{"__ignoreMap":640},[644,1569,1570],{"class":646,"line":647},[644,1571,1573],{"class":1572},"sJ8bj","\u002F**\n",[644,1575,1576],{"class":646,"line":668},[644,1577,1578],{"class":1572}," * Framework-agnostic focus trap utility.\n",[644,1580,1581],{"class":646,"line":683},[644,1582,1583],{"class":1572}," * Attach to the overlay container after it mounts.\n",[644,1585,1586],{"class":646,"line":690},[644,1587,1588],{"class":1572}," * Handles portal boundaries by querying the passed container reference.\n",[644,1590,1591],{"class":646,"line":706},[644,1592,1593],{"class":1572}," *\u002F\n",[644,1595,1596,1598,1600,1603,1605,1608],{"class":646,"line":731},[644,1597,693],{"class":650},[644,1599,696],{"class":650},[644,1601,1602],{"class":699}," createFocusTrap",[644,1604,745],{"class":654},[644,1606,1607],{"class":1255},"container",[644,1609,1299],{"class":654},[644,1611,1612,1614,1617,1619,1622],{"class":646,"line":754},[644,1613,709],{"class":650},[644,1615,1616],{"class":715}," focusableSelectors",[644,1618,739],{"class":650},[644,1620,1621],{"class":661}," 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'",[644,1623,665],{"class":654},[644,1625,1626,1628,1631,1633,1636,1638,1641,1644],{"class":646,"line":759},[644,1627,709],{"class":650},[644,1629,1630],{"class":715}," focusable",[644,1632,739],{"class":650},[644,1634,1635],{"class":654}," Array.",[644,1637,658],{"class":699},[644,1639,1640],{"class":654},"(container.",[644,1642,1643],{"class":699},"querySelectorAll",[644,1645,1646],{"class":654},"(focusableSelectors));\n",[644,1648,1649,1652,1655,1658,1661,1664,1666,1669,1671,1673],{"class":646,"line":774},[644,1650,1651],{"class":650},"  if",[644,1653,1654],{"class":654}," (focusable.",[644,1656,1657],{"class":715},"length",[644,1659,1660],{"class":650}," ===",[644,1662,1663],{"class":715}," 0",[644,1665,1384],{"class":654},[644,1667,1668],{"class":650},"return",[644,1670,1012],{"class":654},[644,1672,768],{"class":650},[644,1674,1675],{"class":654}," {};\n",[644,1677,1678],{"class":646,"line":783},[644,1679,687],{"emptyLinePlaceholder":686},[644,1681,1682,1684,1687,1689,1692,1695],{"class":646,"line":805},[644,1683,709],{"class":650},[644,1685,1686],{"class":715}," first",[644,1688,739],{"class":650},[644,1690,1691],{"class":654}," focusable[",[644,1693,1694],{"class":715},"0",[644,1696,1697],{"class":654},"];\n",[644,1699,1700,1702,1705,1707,1710,1712,1715,1718],{"class":646,"line":822},[644,1701,709],{"class":650},[644,1703,1704],{"class":715}," last",[644,1706,739],{"class":650},[644,1708,1709],{"class":654}," focusable[focusable.",[644,1711,1657],{"class":715},[644,1713,1714],{"class":650}," -",[644,1716,1717],{"class":715}," 1",[644,1719,1697],{"class":654},[644,1721,1722],{"class":646,"line":828},[644,1723,687],{"emptyLinePlaceholder":686},[644,1725,1726,1728,1731,1733,1735,1738,1740,1742],{"class":646,"line":834},[644,1727,709],{"class":650},[644,1729,1730],{"class":699}," handleTab",[644,1732,739],{"class":650},[644,1734,398],{"class":654},[644,1736,1737],{"class":1255},"e",[644,1739,1384],{"class":654},[644,1741,768],{"class":650},[644,1743,771],{"class":654},[644,1745,1746,1748,1751,1754,1757,1759,1761],{"class":646,"line":839},[644,1747,777],{"class":650},[644,1749,1750],{"class":654}," (e.key ",[644,1752,1753],{"class":650},"!==",[644,1755,1756],{"class":661}," 'Tab'",[644,1758,1384],{"class":654},[644,1760,1668],{"class":650},[644,1762,665],{"class":654},[644,1764,1765,1767,1770,1773,1776,1779],{"class":646,"line":871},[644,1766,777],{"class":650},[644,1768,1769],{"class":654}," (e.shiftKey ",[644,1771,1772],{"class":650},"&&",[644,1774,1775],{"class":654}," document.activeElement ",[644,1777,1778],{"class":650},"===",[644,1780,1781],{"class":654}," first) {\n",[644,1783,1784,1787,1790],{"class":646,"line":1097},[644,1785,1786],{"class":654},"      e.",[644,1788,1789],{"class":699},"preventDefault",[644,1791,728],{"class":654},[644,1793,1794,1797,1799],{"class":646,"line":1103},[644,1795,1796],{"class":654},"      last.",[644,1798,810],{"class":699},[644,1800,728],{"class":654},[644,1802,1803,1806,1809,1812,1814,1817,1820,1822,1824,1826],{"class":646,"line":1444},[644,1804,1805],{"class":654},"    } ",[644,1807,1808],{"class":650},"else",[644,1810,1811],{"class":650}," if",[644,1813,398],{"class":654},[644,1815,1816],{"class":650},"!",[644,1818,1819],{"class":654},"e.shiftKey ",[644,1821,1772],{"class":650},[644,1823,1775],{"class":654},[644,1825,1778],{"class":650},[644,1827,1828],{"class":654}," last) {\n",[644,1830,1831,1833,1835],{"class":646,"line":1449},[644,1832,1786],{"class":654},[644,1834,1789],{"class":699},[644,1836,728],{"class":654},[644,1838,1839,1842,1844],{"class":646,"line":1455},[644,1840,1841],{"class":654},"      first.",[644,1843,810],{"class":699},[644,1845,728],{"class":654},[644,1847,1848],{"class":646,"line":1461},[644,1849,825],{"class":654},[644,1851,1852],{"class":646,"line":1466},[644,1853,1854],{"class":654},"  };\n",[644,1856,1857],{"class":646,"line":1474},[644,1858,687],{"emptyLinePlaceholder":686},[644,1860,1861,1864,1867,1869,1872],{"class":646,"line":1487},[644,1862,1863],{"class":654},"  container.",[644,1865,1866],{"class":699},"addEventListener",[644,1868,745],{"class":654},[644,1870,1871],{"class":661},"'keydown'",[644,1873,1874],{"class":654},", handleTab);\n",[644,1876,1877],{"class":646,"line":1499},[644,1878,687],{"emptyLinePlaceholder":686},[644,1880,1881],{"class":646,"line":1504},[644,1882,1883],{"class":1572},"  \u002F\u002F Return cleanup function for framework unmount\n",[644,1885,1887,1889,1891,1893,1896,1899,1901,1903],{"class":646,"line":1886},28,[644,1888,842],{"class":650},[644,1890,1012],{"class":654},[644,1892,768],{"class":650},[644,1894,1895],{"class":654}," container.",[644,1897,1898],{"class":699},"removeEventListener",[644,1900,745],{"class":654},[644,1902,1871],{"class":661},[644,1904,1874],{"class":654},[644,1906,1908],{"class":646,"line":1907},29,[644,1909,874],{"class":654},[324,1911,1912,1913,1915,1916,1919,1920,1923],{},"The snapshot trap above is intentionally minimal. Production overlays are rarely static, so a robust trap recomputes the focusable set when the subtree changes—a date picker that lazily renders its grid, an async list that populates after a fetch, or a disabled submit button that becomes enabled all shift the \"last focusable node.\" The pattern below upgrades the trap to derive boundaries on every ",[345,1914,602],{}," press and to filter out elements that are present in the DOM but not actually focusable (zero-size, ",[345,1917,1918],{},"inert",", or inside a ",[345,1921,1922],{},"hidden"," ancestor).",[635,1925,1927],{"className":921,"code":1926,"language":923,"meta":640,"style":640},"export function createDynamicFocusTrap(container, { onEscape } = {}) {\n  const selector = [\n    'a[href]', 'button:not([disabled])', 'input:not([disabled])',\n    'select:not([disabled])', 'textarea:not([disabled])',\n    '[tabindex]:not([tabindex=\"-1\"])', 'audio[controls]', 'video[controls]',\n  ].join(',');\n\n  const isVisible = (el) =>\n    !el.hasAttribute('inert') &&\n    el.offsetParent !== null &&\n    getComputedStyle(el).visibility !== 'hidden';\n\n  const getFocusable = () =>\n    Array.from(container.querySelectorAll(selector)).filter(isVisible);\n\n  const onKeydown = (e) => {\n    if (e.key === 'Escape') { onEscape?.(); return; }\n    if (e.key !== 'Tab') return;\n\n    const focusable = getFocusable();\n    if (focusable.length === 0) { e.preventDefault(); return; }\n\n    const first = focusable[0];\n    const last = focusable[focusable.length - 1];\n    const active = document.activeElement;\n\n    if (e.shiftKey && (active === first || !container.contains(active))) {\n      e.preventDefault();\n      last.focus();\n    } else if (!e.shiftKey && active === last) {\n      e.preventDefault();\n      first.focus();\n    }\n  };\n\n  container.addEventListener('keydown', onKeydown);\n  return () => container.removeEventListener('keydown', onKeydown);\n}\n",[345,1928,1929,1955,1967,1985,1997,2014,2029,2033,2052,2073,2086,2101,2105,2118,2138,2142,2161,2185,2201,2205,2218,2242,2246,2260,2278,2290,2294,2324,2332,2340,2364,2373,2382,2387,2392,2397,2411,2430],{"__ignoreMap":640},[644,1930,1931,1933,1935,1938,1940,1942,1945,1948,1950,1952],{"class":646,"line":647},[644,1932,693],{"class":650},[644,1934,696],{"class":650},[644,1936,1937],{"class":699}," createDynamicFocusTrap",[644,1939,745],{"class":654},[644,1941,1607],{"class":1255},[644,1943,1944],{"class":654},", { ",[644,1946,1947],{"class":1255},"onEscape",[644,1949,719],{"class":654},[644,1951,722],{"class":650},[644,1953,1954],{"class":654}," {}) {\n",[644,1956,1957,1959,1962,1964],{"class":646,"line":668},[644,1958,709],{"class":650},[644,1960,1961],{"class":715}," selector",[644,1963,739],{"class":650},[644,1965,1966],{"class":654}," [\n",[644,1968,1969,1972,1974,1977,1979,1982],{"class":646,"line":683},[644,1970,1971],{"class":661},"    'a[href]'",[644,1973,797],{"class":654},[644,1975,1976],{"class":661},"'button:not([disabled])'",[644,1978,797],{"class":654},[644,1980,1981],{"class":661},"'input:not([disabled])'",[644,1983,1984],{"class":654},",\n",[644,1986,1987,1990,1992,1995],{"class":646,"line":690},[644,1988,1989],{"class":661},"    'select:not([disabled])'",[644,1991,797],{"class":654},[644,1993,1994],{"class":661},"'textarea:not([disabled])'",[644,1996,1984],{"class":654},[644,1998,1999,2002,2004,2007,2009,2012],{"class":646,"line":706},[644,2000,2001],{"class":661},"    '[tabindex]:not([tabindex=\"-1\"])'",[644,2003,797],{"class":654},[644,2005,2006],{"class":661},"'audio[controls]'",[644,2008,797],{"class":654},[644,2010,2011],{"class":661},"'video[controls]'",[644,2013,1984],{"class":654},[644,2015,2016,2019,2022,2024,2027],{"class":646,"line":731},[644,2017,2018],{"class":654},"  ].",[644,2020,2021],{"class":699},"join",[644,2023,745],{"class":654},[644,2025,2026],{"class":661},"','",[644,2028,751],{"class":654},[644,2030,2031],{"class":646,"line":754},[644,2032,687],{"emptyLinePlaceholder":686},[644,2034,2035,2037,2040,2042,2044,2047,2049],{"class":646,"line":759},[644,2036,709],{"class":650},[644,2038,2039],{"class":699}," isVisible",[644,2041,739],{"class":650},[644,2043,398],{"class":654},[644,2045,2046],{"class":1255},"el",[644,2048,1384],{"class":654},[644,2050,2051],{"class":650},"=>\n",[644,2053,2054,2057,2060,2063,2065,2068,2070],{"class":646,"line":774},[644,2055,2056],{"class":650},"    !",[644,2058,2059],{"class":654},"el.",[644,2061,2062],{"class":699},"hasAttribute",[644,2064,745],{"class":654},[644,2066,2067],{"class":661},"'inert'",[644,2069,1384],{"class":654},[644,2071,2072],{"class":650},"&&\n",[644,2074,2075,2078,2080,2083],{"class":646,"line":783},[644,2076,2077],{"class":654},"    el.offsetParent ",[644,2079,1753],{"class":650},[644,2081,2082],{"class":715}," null",[644,2084,2085],{"class":650}," &&\n",[644,2087,2088,2091,2094,2096,2099],{"class":646,"line":805},[644,2089,2090],{"class":699},"    getComputedStyle",[644,2092,2093],{"class":654},"(el).visibility ",[644,2095,1753],{"class":650},[644,2097,2098],{"class":661}," 'hidden'",[644,2100,665],{"class":654},[644,2102,2103],{"class":646,"line":822},[644,2104,687],{"emptyLinePlaceholder":686},[644,2106,2107,2109,2112,2114,2116],{"class":646,"line":828},[644,2108,709],{"class":650},[644,2110,2111],{"class":699}," getFocusable",[644,2113,739],{"class":650},[644,2115,1012],{"class":654},[644,2117,2051],{"class":650},[644,2119,2120,2123,2125,2127,2129,2132,2135],{"class":646,"line":834},[644,2121,2122],{"class":654},"    Array.",[644,2124,658],{"class":699},[644,2126,1640],{"class":654},[644,2128,1643],{"class":699},[644,2130,2131],{"class":654},"(selector)).",[644,2133,2134],{"class":699},"filter",[644,2136,2137],{"class":654},"(isVisible);\n",[644,2139,2140],{"class":646,"line":839},[644,2141,687],{"emptyLinePlaceholder":686},[644,2143,2144,2146,2149,2151,2153,2155,2157,2159],{"class":646,"line":871},[644,2145,709],{"class":650},[644,2147,2148],{"class":699}," onKeydown",[644,2150,739],{"class":650},[644,2152,398],{"class":654},[644,2154,1737],{"class":1255},[644,2156,1384],{"class":654},[644,2158,768],{"class":650},[644,2160,771],{"class":654},[644,2162,2163,2165,2167,2169,2172,2175,2177,2180,2182],{"class":646,"line":1097},[644,2164,777],{"class":650},[644,2166,1750],{"class":654},[644,2168,1778],{"class":650},[644,2170,2171],{"class":661}," 'Escape'",[644,2173,2174],{"class":654},") { ",[644,2176,1947],{"class":699},[644,2178,2179],{"class":654},"?.(); ",[644,2181,1668],{"class":650},[644,2183,2184],{"class":654},"; }\n",[644,2186,2187,2189,2191,2193,2195,2197,2199],{"class":646,"line":1103},[644,2188,777],{"class":650},[644,2190,1750],{"class":654},[644,2192,1753],{"class":650},[644,2194,1756],{"class":661},[644,2196,1384],{"class":654},[644,2198,1668],{"class":650},[644,2200,665],{"class":654},[644,2202,2203],{"class":646,"line":1444},[644,2204,687],{"emptyLinePlaceholder":686},[644,2206,2207,2210,2212,2214,2216],{"class":646,"line":1449},[644,2208,2209],{"class":650},"    const",[644,2211,1630],{"class":715},[644,2213,739],{"class":650},[644,2215,2111],{"class":699},[644,2217,728],{"class":654},[644,2219,2220,2222,2224,2226,2228,2230,2233,2235,2238,2240],{"class":646,"line":1455},[644,2221,777],{"class":650},[644,2223,1654],{"class":654},[644,2225,1657],{"class":715},[644,2227,1660],{"class":650},[644,2229,1663],{"class":715},[644,2231,2232],{"class":654},") { e.",[644,2234,1789],{"class":699},[644,2236,2237],{"class":654},"(); ",[644,2239,1668],{"class":650},[644,2241,2184],{"class":654},[644,2243,2244],{"class":646,"line":1461},[644,2245,687],{"emptyLinePlaceholder":686},[644,2247,2248,2250,2252,2254,2256,2258],{"class":646,"line":1466},[644,2249,2209],{"class":650},[644,2251,1686],{"class":715},[644,2253,739],{"class":650},[644,2255,1691],{"class":654},[644,2257,1694],{"class":715},[644,2259,1697],{"class":654},[644,2261,2262,2264,2266,2268,2270,2272,2274,2276],{"class":646,"line":1474},[644,2263,2209],{"class":650},[644,2265,1704],{"class":715},[644,2267,739],{"class":650},[644,2269,1709],{"class":654},[644,2271,1657],{"class":715},[644,2273,1714],{"class":650},[644,2275,1717],{"class":715},[644,2277,1697],{"class":654},[644,2279,2280,2282,2285,2287],{"class":646,"line":1487},[644,2281,2209],{"class":650},[644,2283,2284],{"class":715}," active",[644,2286,739],{"class":650},[644,2288,2289],{"class":654}," document.activeElement;\n",[644,2291,2292],{"class":646,"line":1499},[644,2293,687],{"emptyLinePlaceholder":686},[644,2295,2296,2298,2300,2302,2305,2307,2310,2312,2315,2318,2321],{"class":646,"line":1504},[644,2297,777],{"class":650},[644,2299,1769],{"class":654},[644,2301,1772],{"class":650},[644,2303,2304],{"class":654}," (active ",[644,2306,1778],{"class":650},[644,2308,2309],{"class":654}," first ",[644,2311,1387],{"class":650},[644,2313,2314],{"class":650}," !",[644,2316,2317],{"class":654},"container.",[644,2319,2320],{"class":699},"contains",[644,2322,2323],{"class":654},"(active))) {\n",[644,2325,2326,2328,2330],{"class":646,"line":1886},[644,2327,1786],{"class":654},[644,2329,1789],{"class":699},[644,2331,728],{"class":654},[644,2333,2334,2336,2338],{"class":646,"line":1907},[644,2335,1796],{"class":654},[644,2337,810],{"class":699},[644,2339,728],{"class":654},[644,2341,2343,2345,2347,2349,2351,2353,2355,2357,2360,2362],{"class":646,"line":2342},30,[644,2344,1805],{"class":654},[644,2346,1808],{"class":650},[644,2348,1811],{"class":650},[644,2350,398],{"class":654},[644,2352,1816],{"class":650},[644,2354,1819],{"class":654},[644,2356,1772],{"class":650},[644,2358,2359],{"class":654}," active ",[644,2361,1778],{"class":650},[644,2363,1828],{"class":654},[644,2365,2367,2369,2371],{"class":646,"line":2366},31,[644,2368,1786],{"class":654},[644,2370,1789],{"class":699},[644,2372,728],{"class":654},[644,2374,2376,2378,2380],{"class":646,"line":2375},32,[644,2377,1841],{"class":654},[644,2379,810],{"class":699},[644,2381,728],{"class":654},[644,2383,2385],{"class":646,"line":2384},33,[644,2386,825],{"class":654},[644,2388,2390],{"class":646,"line":2389},34,[644,2391,1854],{"class":654},[644,2393,2395],{"class":646,"line":2394},35,[644,2396,687],{"emptyLinePlaceholder":686},[644,2398,2400,2402,2404,2406,2408],{"class":646,"line":2399},36,[644,2401,1863],{"class":654},[644,2403,1866],{"class":699},[644,2405,745],{"class":654},[644,2407,1871],{"class":661},[644,2409,2410],{"class":654},", onKeydown);\n",[644,2412,2414,2416,2418,2420,2422,2424,2426,2428],{"class":646,"line":2413},37,[644,2415,842],{"class":650},[644,2417,1012],{"class":654},[644,2419,768],{"class":650},[644,2421,1895],{"class":654},[644,2423,1898],{"class":699},[644,2425,745],{"class":654},[644,2427,1871],{"class":661},[644,2429,2410],{"class":654},[644,2431,2433],{"class":646,"line":2432},38,[644,2434,874],{"class":654},[324,2436,2437,2438,2441,2442,2445,2446,2448,2449,2451,2452,2454,2455,2458,2459,593],{},"Two details earn their keep here. First, the ",[345,2439,2440],{},"!container.contains(active)"," check repairs a trap whose focus has somehow escaped (for example after a background ",[345,2443,2444],{},"autofocus"," fired), pulling the cursor back inside on the next ",[345,2447,602],{},". Second, wiring ",[345,2450,397],{}," into the same handler keeps dismissal logic co-located with the trap, which makes it trivial to invoke ",[345,2453,587],{}," on the stored trigger immediately afterward. For the broader set of keyboard interactions that overlays demand—arrow-key roving, type-ahead, and ",[345,2456,2457],{},"aria-activedescendant","—see ",[328,2460,2462],{"href":2461},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002F","Keyboard navigation patterns for modals",[324,2464,2465,2466,2468,2469,2472,2473,2476,2477,2479],{},"A complete overlay also neutralizes the background. Modern browsers support the ",[345,2467,1918],{}," attribute, which removes an entire subtree from the tab order and the accessibility tree in one line—",[345,2470,2471],{},"appRoot.inert = true"," while the dialog is open, then ",[345,2474,2475],{},"false"," on close. This is dramatically more reliable than manually toggling ",[345,2478,632],{}," on every background control, and it composes cleanly with the trap above.",[324,2481,2482,2484],{},[335,2483,598],{}," Test with screen readers (NVDA\u002FVoiceOver) to ensure the focus loop does not break when overlay content updates dynamically or when nested interactive widgets (like date pickers) are rendered inside the trap.",[383,2486,2488],{"id":2487},"dynamic-content-injection-live-region-coordination","Dynamic Content Injection & Live Region Coordination",[324,2490,2491,2492,2495],{},"Asynchronous data fetching and skeleton-to-content transitions frequently cause focus loss. Moving focus to newly injected content should only occur when the update demands immediate user interaction (e.g., form validation errors, search results). For background updates, rely on ",[345,2493,2494],{},"aria-live=\"polite\""," to announce changes without hijacking the keyboard cursor.",[324,2497,2498,2499,2502,2503,2506],{},"The decision rule is worth internalizing: ",[335,2500,2501],{},"move focus only when the user's next logical action lives inside the new content."," Submitting a form that surfaces an inline error? Move focus to the error summary so the user can act on it. Loading the next page of an infinite-scroll feed? Do ",[391,2504,2505],{},"not"," move focus—announce the count politely and let the user continue reading where they were. Conflating these two cases is the single most common cause of \"the screen reader keeps jumping around\" complaints in SPAs.",[324,2508,2509],{},"When you do move focus into freshly injected content, sequence the work so the announcement and the focus shift do not collide. A reliable order is: render the content, set the live-region text, then in the next frame move focus to the target. Forcing the live-region update before the focus call lets the screen reader queue the announcement politely instead of having it clipped by the focus event.",[324,2511,2512,2513,2516,2517,2520,2521,2523,2524,2527],{},"Visual focus indicators must remain highly visible during these transitions. Ensure your theming system does not suppress ",[345,2514,2515],{},":focus-visible"," outlines when switching between light\u002Fdark modes or applying CSS-in-JS overrides. A frequent regression is a global ",[345,2518,2519],{},"*:focus { outline: none }"," reset that was never paired with a ",[345,2522,2515],{}," replacement; the result passes a casual mouse test but strands keyboard users invisibly. Coordinate your focus states with ",[328,2525,13],{"href":2526},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002F"," to guarantee compliance across all visual contexts.",[324,2529,2530,2532,2533,2536],{},[335,2531,598],{}," Validate that auto-focus does not interrupt screen reader announcements and that focus remains predictable during rapid state updates. Use axe DevTools to verify ",[345,2534,2535],{},"aria-live"," regions are correctly announced without stealing focus.",[383,2538,2540],{"id":2539},"storing-and-restoring-the-trigger","Storing and Restoring the Trigger",[324,2542,2543,2544,2546,2547,2550],{},"The arrow in the lifecycle diagram that returns focus to the trigger is easy to draw and easy to forget. The mechanism is simple: capture ",[345,2545,1537],{}," at the moment the overlay opens, and call ",[345,2548,2549],{},".focus()"," on it when the overlay closes. The traps are in the details—the trigger may have unmounted by the time the overlay closes (think a \"Delete\" button that disappears after the confirmed deletion), so always provide a fallback target.",[635,2552,2554],{"className":921,"code":2553,"language":923,"meta":640,"style":640},"export function useFocusRestore() {\n  let trigger = null;\n\n  const remember = () => {\n    trigger = document.activeElement;\n  };\n\n  const restore = (fallbackSelector = 'main, h1') => {\n    if (trigger && document.contains(trigger) && trigger.offsetParent !== null) {\n      trigger.focus();\n    } else {\n      const fallback = document.querySelector(fallbackSelector);\n      fallback?.setAttribute('tabindex', '-1');\n      fallback?.focus();\n    }\n    trigger = null;\n  };\n\n  return { remember, restore };\n}\n",[345,2555,2556,2567,2581,2585,2600,2609,2613,2617,2642,2669,2678,2686,2702,2719,2727,2731,2741,2745,2749,2756],{"__ignoreMap":640},[644,2557,2558,2560,2562,2565],{"class":646,"line":647},[644,2559,693],{"class":650},[644,2561,696],{"class":650},[644,2563,2564],{"class":699}," useFocusRestore",[644,2566,703],{"class":654},[644,2568,2569,2572,2575,2577,2579],{"class":646,"line":668},[644,2570,2571],{"class":650},"  let",[644,2573,2574],{"class":654}," trigger ",[644,2576,722],{"class":650},[644,2578,2082],{"class":715},[644,2580,665],{"class":654},[644,2582,2583],{"class":646,"line":683},[644,2584,687],{"emptyLinePlaceholder":686},[644,2586,2587,2589,2592,2594,2596,2598],{"class":646,"line":690},[644,2588,709],{"class":650},[644,2590,2591],{"class":699}," remember",[644,2593,739],{"class":650},[644,2595,1012],{"class":654},[644,2597,768],{"class":650},[644,2599,771],{"class":654},[644,2601,2602,2605,2607],{"class":646,"line":706},[644,2603,2604],{"class":654},"    trigger ",[644,2606,722],{"class":650},[644,2608,2289],{"class":654},[644,2610,2611],{"class":646,"line":731},[644,2612,1854],{"class":654},[644,2614,2615],{"class":646,"line":754},[644,2616,687],{"emptyLinePlaceholder":686},[644,2618,2619,2621,2624,2626,2628,2631,2633,2636,2638,2640],{"class":646,"line":759},[644,2620,709],{"class":650},[644,2622,2623],{"class":699}," restore",[644,2625,739],{"class":650},[644,2627,398],{"class":654},[644,2629,2630],{"class":1255},"fallbackSelector",[644,2632,739],{"class":650},[644,2634,2635],{"class":661}," 'main, h1'",[644,2637,1384],{"class":654},[644,2639,768],{"class":650},[644,2641,771],{"class":654},[644,2643,2644,2646,2649,2651,2653,2655,2658,2660,2663,2665,2667],{"class":646,"line":774},[644,2645,777],{"class":650},[644,2647,2648],{"class":654}," (trigger ",[644,2650,1772],{"class":650},[644,2652,1039],{"class":654},[644,2654,2320],{"class":699},[644,2656,2657],{"class":654},"(trigger) ",[644,2659,1772],{"class":650},[644,2661,2662],{"class":654}," trigger.offsetParent ",[644,2664,1753],{"class":650},[644,2666,2082],{"class":715},[644,2668,1299],{"class":654},[644,2670,2671,2674,2676],{"class":646,"line":783},[644,2672,2673],{"class":654},"      trigger.",[644,2675,810],{"class":699},[644,2677,728],{"class":654},[644,2679,2680,2682,2684],{"class":646,"line":805},[644,2681,1805],{"class":654},[644,2683,1808],{"class":650},[644,2685,771],{"class":654},[644,2687,2688,2690,2693,2695,2697,2699],{"class":646,"line":822},[644,2689,1031],{"class":650},[644,2691,2692],{"class":715}," fallback",[644,2694,739],{"class":650},[644,2696,1039],{"class":654},[644,2698,914],{"class":699},[644,2700,2701],{"class":654},"(fallbackSelector);\n",[644,2703,2704,2707,2709,2711,2713,2715,2717],{"class":646,"line":828},[644,2705,2706],{"class":654},"      fallback?.",[644,2708,789],{"class":699},[644,2710,745],{"class":654},[644,2712,794],{"class":661},[644,2714,797],{"class":654},[644,2716,800],{"class":661},[644,2718,751],{"class":654},[644,2720,2721,2723,2725],{"class":646,"line":834},[644,2722,2706],{"class":654},[644,2724,810],{"class":699},[644,2726,728],{"class":654},[644,2728,2729],{"class":646,"line":839},[644,2730,825],{"class":654},[644,2732,2733,2735,2737,2739],{"class":646,"line":871},[644,2734,2604],{"class":654},[644,2736,722],{"class":650},[644,2738,2082],{"class":715},[644,2740,665],{"class":654},[644,2742,2743],{"class":646,"line":1097},[644,2744,1854],{"class":654},[644,2746,2747],{"class":646,"line":1103},[644,2748,687],{"emptyLinePlaceholder":686},[644,2750,2751,2753],{"class":646,"line":1444},[644,2752,842],{"class":650},[644,2754,2755],{"class":654}," { remember, restore };\n",[644,2757,2758],{"class":646,"line":1449},[644,2759,874],{"class":654},[324,2761,2762,2763,2766,2767,2769,2770,2773],{},"Checking ",[345,2764,2765],{},"document.contains(trigger)"," guards against restoring focus to a detached node—a silent failure that drops the user back on ",[345,2768,572],{},". The visibility check (",[345,2771,2772],{},"offsetParent !== null",") covers the case where the trigger is technically still in the DOM but hidden behind a collapsed accordion or an off-screen tab panel.",[383,2775,2777],{"id":2776},"common-pitfalls","Common Pitfalls",[339,2779,2780,2788,2812,2818,2827,2833],{},[342,2781,2782,2785,2786,593],{},[335,2783,2784],{},"Unintended Auto-Focus:"," Auto-focusing inputs on route change without explicit user intent causes screen reader context loss and violates ",[345,2787,352],{},[342,2789,2790,2797,2798,2801,2802,797,2805,2808,2809,2811],{},[335,2791,2792,2793,2796],{},"Incorrect ",[345,2794,2795],{},"tabindex"," Usage:"," Applying ",[345,2799,2800],{},"tabindex=\"0\""," to non-interactive containers (",[345,2803,2804],{},"\u003Cdiv>",[345,2806,2807],{},"\u003Cmain>",") pollutes the tab order. Always use ",[345,2810,632],{}," for programmatic focus targets.",[342,2813,2814,2817],{},[335,2815,2816],{},"Missing Focus Restoration:"," Failing to return focus to the trigger element after closing a modal or dropdown leaves keyboard users stranded in the DOM.",[342,2819,2820,2823,2824,2826],{},[335,2821,2822],{},"CSS Focus Stripping:"," Relying solely on CSS ",[345,2825,2515],{}," without verifying that framework state updates (e.g., class toggling, hydration mismatches) don't strip outline styles.",[342,2828,2829,2832],{},[335,2830,2831],{},"Shadow DOM & Portal Blind Spots:"," Implementing focus traps that ignore shadow DOM boundaries or portal containers, causing the trap to query the wrong subtree and fail.",[342,2834,2835,2838],{},[335,2836,2837],{},"Stale Focusable Snapshots:"," Computing first\u002Flast focusable nodes once on mount, then breaking the loop when the overlay's interactive content changes asynchronously.",[383,2840,2842],{"id":2841},"how-to-verify-focus-management","How to Verify Focus Management",[324,2844,2845],{},"Automated tools catch structural problems; only manual testing catches the experience. Run both.",[339,2847,2848,2864,2873],{},[342,2849,2850,2853,2854,2857,2858,2860,2861,2863],{},[335,2851,2852],{},"Automated:"," Run axe DevTools or the ",[345,2855,2856],{},"@axe-core\u002Fplaywright"," integration against each route and each open-overlay state. These flag orphaned ",[345,2859,2795],{},", missing accessible names on focus targets, and live-region misconfiguration. In CI, assert ",[345,2862,1537],{}," after scripted navigations and after opening\u002Fclosing every dialog.",[342,2865,2866,2869,2870,2872],{},[335,2867,2868],{},"Manual keyboard pass:"," Unplug the mouse. Tab through a route change and confirm focus lands on the new view's heading. Open every overlay, Tab to the last control, Tab again, and confirm focus wraps to the first control—then press ",[345,2871,397],{}," and confirm focus returns to the trigger.",[342,2874,2875,2878],{},[335,2876,2877],{},"Manual screen-reader pass:"," With NVDA (Windows) or VoiceOver (macOS), navigate between routes and confirm the new page is announced exactly once. Open a dialog and confirm the screen reader enters it and cannot read background content while it is open.",[383,2880,2882],{"id":2881},"frequently-asked-questions","Frequently Asked Questions",[324,2884,2885,2888],{},[335,2886,2887],{},"Should I auto-focus the first input on every SPA route change?","\nNo. Auto-focusing should only occur when the new route's primary action is form completion or immediate data entry. Otherwise, shift focus to a heading or landmark to preserve context and avoid disrupting screen reader navigation flows.",[324,2890,2891,2894,2895,2898,2899,2901],{},[335,2892,2893],{},"How do I handle focus when using CSS-in-JS or styled components?","\nEnsure your styling system preserves native ",[345,2896,2897],{},":focus"," and ",[345,2900,2515],{}," states. Framework-specific focus management relies on DOM element references, so CSS injection timing must not delay focus application or strip outline styles during hydration or re-renders.",[324,2903,2904,2907],{},[335,2905,2906],{},"What is the difference between focus trapping and focus management?","\nFocus management is the overarching strategy for directing keyboard navigation across the entire application lifecycle (routing, state changes, dynamic updates). Focus trapping is a specific technique used to contain focus within a temporary UI layer, like a modal or dialog, until it is dismissed.",[324,2909,2910,2916,2917,2919,2920,2922,2923,2925],{},[335,2911,2912,2913,2915],{},"Should I use the ",[345,2914,1918],{}," attribute or a focus trap for modals?","\nUse both. A focus trap keeps ",[345,2918,602],{}," cycling inside the dialog, while ",[345,2921,1918],{}," on the background root removes those controls from the accessibility tree entirely so a screen reader's virtual cursor cannot read them either. Together they deliver the complete \"the rest of the page does not exist\" experience that ",[345,2924,401],{}," expects.",[324,2927,2928,2931,2932,2934,2935,2937,2938,593],{},[335,2929,2930],{},"Where do I store the element to restore focus to after a modal closes?","\nCapture ",[345,2933,1537],{}," at the instant the modal opens and keep it in a ref or closure. On close, verify the stored node is still in the document and visible before focusing it; if it was removed, fall back to the nearest stable landmark such as ",[345,2936,2807],{}," or the page ",[345,2939,606],{},[324,2941,2942,2948,2949,2951,2952,2955,2956,2958],{},[335,2943,2944,2945,2947],{},"Does moving focus on route change satisfy ",[345,2946,352],{}," by itself?","\nIt is necessary but not sufficient. ",[345,2950,401],{}," requires that the ",[391,2953,2954],{},"sequence"," of focusable elements preserves meaning and operability. Landing focus on the main heading is the right entry point, but you must also ensure the subsequent tab order through the new view is logical and that no stray ",[345,2957,2795],{}," values reorder it.",[383,2960,2962],{"id":2961},"related-guides","Related guides",[339,2964,2965,2969,2973,2977],{},[342,2966,2967],{},[328,2968,10],{"href":330},[342,2970,2971],{},[328,2972,592],{"href":591},[342,2974,2975],{},[328,2976,2462],{"href":2461},[342,2978,2979],{},[328,2980,2982],{"href":2981},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F","React hooks for accessibility",[2984,2985,2986],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":640,"searchDepth":668,"depth":668,"links":2988},[2989,2990,2991,2996,2997,2998,2999,3000,3001,3002],{"id":385,"depth":668,"text":386},{"id":558,"depth":668,"text":559},{"id":610,"depth":668,"text":611,"children":2992},[2993,2994,2995],{"id":618,"depth":683,"text":619},{"id":903,"depth":683,"text":904},{"id":1137,"depth":683,"text":1138},{"id":1541,"depth":668,"text":1542},{"id":2487,"depth":668,"text":2488},{"id":2539,"depth":668,"text":2540},{"id":2776,"depth":668,"text":2777},{"id":2841,"depth":668,"text":2842},{"id":2881,"depth":668,"text":2882},{"id":2961,"depth":668,"text":2962},null,"Fix lost focus after SPA route changes with proven patterns for focus restoration, modal handling, and keyboard-safe navigation in dynamic interfaces.","md",{},false,{"title":25,"description":3004},"ENDIbifwJTIb_FcpY0UcJ7KCDb-H6wYt9jAyublRx2w",[3011,3050,3051,3114],{"title":5,"path":6,"stem":7,"children":3012,"page":-1},[3013,3014,3017,3020,3026,3032,3041,3047],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":3015},[3016],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":3018},[3019],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":3021,"page":-1},[3022,3023],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":3024},[3025],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":3027,"page":-1},[3028,3029],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":3030},[3031],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":3033,"page":-1},[3034,3035,3038],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":3036},[3037],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":3039},[3040],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":3042},[3043,3044],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":3045},[3046],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":3048},[3049],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":3052,"page":-1},[3053,3054,3060,3072,3084,3087,3096,3108],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":3055,"page":-1},[3056,3057],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":3058},[3059],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":3061,"page":-1},[3062,3063,3066,3069],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":3064},[3065],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":3067},[3068],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":3070},[3071],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":3073,"page":-1},[3074,3075,3078,3081],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":3076},[3077],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":3079},[3080],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":3082},[3083],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":3085},[3086],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":3088,"page":-1},[3089,3090,3093],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":3091},[3092],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":3094},[3095],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":3097,"page":-1},[3098,3099,3102,3105],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":3100},[3101],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":3103},[3104],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":3106},[3107],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":3109,"page":-1},[3110,3111],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":3112},[3113],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":3115,"page":-1},[3116,3117,3126,3135,3144,3153],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":3118},[3119,3120,3123],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":3121},[3122],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":3124},[3125],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":3127,"page":-1},[3128,3129,3132],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":3130},[3131],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":3133},[3134],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":3136,"page":-1},[3137,3138,3141],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":3139},[3140],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":3142},[3143],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":3145,"page":-1},[3146,3147,3150],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":3148},[3149],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":3151},[3152],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":3154,"page":-1},[3155,3156,3159],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":3157},[3158],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":3160},[3161],{"title":309,"path":310,"stem":311},1781785523210]