[{"data":1,"prerenderedAt":1461},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002F":156,"content-navigation":1387},[4,66,70],{"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,60],{"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 in Modern Frameworks","\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},"Screen Reader Compatibility Testing for Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[53,54],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"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",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"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",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},"Modern Framework Accessibility","\u002F","index",{"title":71,"path":72,"stem":73,"children":74},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[75,78,90,102,108,126,144],{"title":76,"path":72,"stem":77},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":79,"path":80,"stem":81,"children":82},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[83,84],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"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",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Dynamic Content & State Announcements in React & Next.js","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[95,96],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"Implementing React Context for Global 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",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Form Handling with React Hook Form & Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Next.js App Router & A11y: Implementation Guide for Modern Frameworks","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[113,114,120],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Implementing Skip Links in Next.js App Router: A Step-by-Step Guide","\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",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Next.js Dynamic Imports and Keyboard Navigation: A Complete A11y Implementation Guide","\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",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"React Hooks for Accessibility: Implementation Patterns & State Management","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[131,132,138],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"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",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"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",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[149,150],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Handling 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",[155],{"title":151,"path":152,"stem":153},{"id":157,"title":121,"body":158,"date":1380,"description":1381,"extension":1382,"image":1380,"meta":1383,"modifiedAt":1380,"navigation":333,"noindex":1384,"path":122,"publishedAt":1380,"seo":1385,"stem":123,"updatedAt":1380,"__hash__":1386},"content\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002Findex.md",{"type":159,"value":160,"toc":1356},"minimark",[161,165,183,188,217,221,235,238,243,262,266,296,540,544,576,677,679,683,690,693,722,907,910,942,1034,1036,1040,1043,1046,1074,1209,1212,1244,1246,1250,1308,1310,1314,1321,1324,1328,1337,1345,1352],[162,163,121],"h1",{"id":164},"nextjs-dynamic-imports-and-keyboard-navigation-a-complete-a11y-implementation-guide",[166,167,168,169,173,174,178,179,182],"p",{},"Implementing lazy-loaded components in Next.js frequently breaks keyboard focus and screen reader announcements. This guide demonstrates how to pair ",[170,171,172],"code",{},"next\u002Fdynamic"," with robust focus management, ensuring seamless navigation across deferred UI. For foundational routing principles, review ",[175,176,76],"a",{"href":177},"\u002Freact-nextjs-accessibility-patterns\u002F"," before diving into component-level optimizations. We cover ",[170,180,181],{},"Suspense"," fallbacks, programmatic focus restoration, and ARIA live regions to maintain compliance while preserving performance.",[184,185,187],"h3",{"id":186},"wcag-compliance-mapping","WCAG Compliance Mapping",[189,190,191,199,205,211],"ul",{},[192,193,194,198],"li",{},[195,196,197],"strong",{},"2.1.1 (Keyboard):"," Ensures all interactive elements remain reachable via standard tab navigation.",[192,200,201,204],{},[195,202,203],{},"2.4.3 (Focus Order):"," Maintains logical DOM sequence during asynchronous rendering.",[192,206,207,210],{},[195,208,209],{},"4.1.2 (Name, Role, Value):"," Preserves semantic structure in loading placeholders.",[192,212,213,216],{},[195,214,215],{},"1.3.1 (Info and Relationships):"," Uses ARIA states to communicate dynamic content changes.",[184,218,220],{"id":219},"core-implementation-principles","Core Implementation Principles",[189,222,223,226,229,232],{},[192,224,225],{},"Dynamic imports must preserve tab order and visible focus indicators.",[192,227,228],{},"Loading placeholders require semantic structure and explicit ARIA states.",[192,230,231],{},"Programmatic focus restoration prevents spatial disorientation.",[192,233,234],{},"Screen reader announcements must remain polite and non-interruptive.",[236,237],"hr",{},[239,240,242],"h2",{"id":241},"configuring-nextdynamic-for-accessible-loading-states","Configuring next\u002Fdynamic for Accessible Loading States",[166,244,245,246,248,249,252,253,256,257,261],{},"The ",[170,247,172],{}," API accepts a ",[170,250,251],{},"loading"," prop that renders while the chunk resolves. Default implementations often use empty ",[170,254,255],{},"\u003Cdiv>"," elements that steal focus or disrupt the tab sequence. Replace generic wrappers with semantic, non-interactive placeholders that explicitly communicate state to assistive technology. This approach aligns with deferred rendering strategies documented in ",[175,258,260],{"href":259},"\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002F","Next.js App Router & A11y",".",[184,263,265],{"id":264},"implementation-steps","Implementation Steps",[267,268,269,275,282,289],"ol",{},[192,270,271,272,274],{},"Pass an accessible component to the ",[170,273,251],{}," property.",[192,276,277,278,281],{},"Apply ",[170,279,280],{},"aria-busy=\"true\""," to the container to signal asynchronous content loading.",[192,283,284,285,288],{},"Include a visually hidden label using ",[170,286,287],{},".sr-only"," for screen readers.",[192,290,291,292,295],{},"Set ",[170,293,294],{},"ssr: false"," only when client-side hydration is strictly required to avoid hydration mismatches.",[297,298,303],"pre",{"className":299,"code":300,"language":301,"meta":302,"style":302},"language-tsx shiki shiki-themes github-light github-dark","import dynamic from 'next\u002Fdynamic';\n\nconst HeavyComponent = dynamic(() => import('.\u002FHeavyComponent'), {\n loading: () => (\n \u003Cdiv aria-busy=\"true\" role=\"status\">\n \u003Cspan className=\"sr-only\">Loading component...\u003C\u002Fspan>\n \u003C\u002Fdiv>\n ),\n ssr: false\n});\n\nexport default function AccessibleLazyPage() {\n return (\n \u003Cmain>\n \u003CHeavyComponent \u002F>\n \u003C\u002Fmain>\n );\n}\n","tsx","",[170,304,305,328,335,370,384,414,436,446,452,461,467,472,490,498,508,519,528,534],{"__ignoreMap":302},[306,307,310,314,318,321,325],"span",{"class":308,"line":309},"line",1,[306,311,313],{"class":312},"szBVR","import",[306,315,317],{"class":316},"sVt8B"," dynamic ",[306,319,320],{"class":312},"from",[306,322,324],{"class":323},"sZZnC"," 'next\u002Fdynamic'",[306,326,327],{"class":316},";\n",[306,329,331],{"class":308,"line":330},2,[306,332,334],{"emptyLinePlaceholder":333},true,"\n",[306,336,338,341,345,348,352,355,358,361,364,367],{"class":308,"line":337},3,[306,339,340],{"class":312},"const",[306,342,344],{"class":343},"sj4cs"," HeavyComponent",[306,346,347],{"class":312}," =",[306,349,351],{"class":350},"sScJk"," dynamic",[306,353,354],{"class":316},"(() ",[306,356,357],{"class":312},"=>",[306,359,360],{"class":312}," import",[306,362,363],{"class":316},"(",[306,365,366],{"class":323},"'.\u002FHeavyComponent'",[306,368,369],{"class":316},"), {\n",[306,371,373,376,379,381],{"class":308,"line":372},4,[306,374,375],{"class":350}," loading",[306,377,378],{"class":316},": () ",[306,380,357],{"class":312},[306,382,383],{"class":316}," (\n",[306,385,387,390,394,397,400,403,406,408,411],{"class":308,"line":386},5,[306,388,389],{"class":316}," \u003C",[306,391,393],{"class":392},"s9eBZ","div",[306,395,396],{"class":350}," aria-busy",[306,398,399],{"class":312},"=",[306,401,402],{"class":323},"\"true\"",[306,404,405],{"class":350}," role",[306,407,399],{"class":312},[306,409,410],{"class":323},"\"status\"",[306,412,413],{"class":316},">\n",[306,415,417,419,421,424,426,429,432,434],{"class":308,"line":416},6,[306,418,389],{"class":316},[306,420,306],{"class":392},[306,422,423],{"class":350}," className",[306,425,399],{"class":312},[306,427,428],{"class":323},"\"sr-only\"",[306,430,431],{"class":316},">Loading component...\u003C\u002F",[306,433,306],{"class":392},[306,435,413],{"class":316},[306,437,439,442,444],{"class":308,"line":438},7,[306,440,441],{"class":316}," \u003C\u002F",[306,443,393],{"class":392},[306,445,413],{"class":316},[306,447,449],{"class":308,"line":448},8,[306,450,451],{"class":316}," ),\n",[306,453,455,458],{"class":308,"line":454},9,[306,456,457],{"class":316}," ssr: ",[306,459,460],{"class":343},"false\n",[306,462,464],{"class":308,"line":463},10,[306,465,466],{"class":316},"});\n",[306,468,470],{"class":308,"line":469},11,[306,471,334],{"emptyLinePlaceholder":333},[306,473,475,478,481,484,487],{"class":308,"line":474},12,[306,476,477],{"class":312},"export",[306,479,480],{"class":312}," default",[306,482,483],{"class":312}," function",[306,485,486],{"class":350}," AccessibleLazyPage",[306,488,489],{"class":316},"() {\n",[306,491,493,496],{"class":308,"line":492},13,[306,494,495],{"class":312}," return",[306,497,383],{"class":316},[306,499,501,503,506],{"class":308,"line":500},14,[306,502,389],{"class":316},[306,504,505],{"class":392},"main",[306,507,413],{"class":316},[306,509,511,513,516],{"class":308,"line":510},15,[306,512,389],{"class":316},[306,514,515],{"class":343},"HeavyComponent",[306,517,518],{"class":316}," \u002F>\n",[306,520,522,524,526],{"class":308,"line":521},16,[306,523,441],{"class":316},[306,525,505],{"class":392},[306,527,413],{"class":316},[306,529,531],{"class":308,"line":530},17,[306,532,533],{"class":316}," );\n",[306,535,537],{"class":308,"line":536},18,[306,538,539],{"class":316},"}\n",[184,541,543],{"id":542},"debugging-ci-testing-workflow","Debugging & CI Testing Workflow",[189,545,546,552,562],{},[192,547,548,551],{},[195,549,550],{},"Screen Reader Verification:"," Run VoiceOver (macOS) or NVDA (Windows). Confirm the loading state is announced without interrupting the current focus context.",[192,553,554,557,558,561],{},[195,555,556],{},"Keyboard Navigation:"," Press ",[170,559,560],{},"Tab"," repeatedly. Ensure focus skips the placeholder entirely and does not trap on non-interactive elements.",[192,563,564,567,568,571,572,575],{},[195,565,566],{},"CI Integration:"," Add ",[170,569,570],{},"@axe-core\u002Freact"," to your test suite. Configure Jest to fail builds if ",[170,573,574],{},"aria-busy"," is applied to interactive elements or if loading placeholders lack accessible names.",[297,577,581],{"className":578,"code":579,"language":580,"meta":302,"style":302},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F jest.config.js snippet\nmodule.exports = {\nsetupFilesAfterEnv: ['\u003CrootDir>\u002Fjest.setup.js'],\ntestEnvironment: 'jsdom',\n};\n\u002F\u002F jest.setup.js\nimport { configureAxe } from 'jest-axe';\nconst axe = configureAxe({ rules: { 'aria-busy': { enabled: true } } });\n","js",[170,582,583,589,604,615,626,631,636,650],{"__ignoreMap":302},[306,584,585],{"class":308,"line":309},[306,586,588],{"class":587},"sJ8bj","\u002F\u002F jest.config.js snippet\n",[306,590,591,594,596,599,601],{"class":308,"line":330},[306,592,593],{"class":343},"module",[306,595,261],{"class":316},[306,597,598],{"class":343},"exports",[306,600,347],{"class":312},[306,602,603],{"class":316}," {\n",[306,605,606,609,612],{"class":308,"line":337},[306,607,608],{"class":316},"setupFilesAfterEnv: [",[306,610,611],{"class":323},"'\u003CrootDir>\u002Fjest.setup.js'",[306,613,614],{"class":316},"],\n",[306,616,617,620,623],{"class":308,"line":372},[306,618,619],{"class":316},"testEnvironment: ",[306,621,622],{"class":323},"'jsdom'",[306,624,625],{"class":316},",\n",[306,627,628],{"class":308,"line":386},[306,629,630],{"class":316},"};\n",[306,632,633],{"class":308,"line":416},[306,634,635],{"class":587},"\u002F\u002F jest.setup.js\n",[306,637,638,640,643,645,648],{"class":308,"line":438},[306,639,313],{"class":312},[306,641,642],{"class":316}," { configureAxe } ",[306,644,320],{"class":312},[306,646,647],{"class":323}," 'jest-axe'",[306,649,327],{"class":316},[306,651,652,654,657,659,662,665,668,671,674],{"class":308,"line":448},[306,653,340],{"class":312},[306,655,656],{"class":343}," axe",[306,658,347],{"class":312},[306,660,661],{"class":350}," configureAxe",[306,663,664],{"class":316},"({ rules: { ",[306,666,667],{"class":323},"'aria-busy'",[306,669,670],{"class":316},": { enabled: ",[306,672,673],{"class":343},"true",[306,675,676],{"class":316}," } } });\n",[236,678],{},[239,680,682],{"id":681},"managing-focus-after-dynamic-component-mount","Managing Focus After Dynamic Component Mount",[166,684,685,686,689],{},"When a lazy component replaces a loading skeleton, the browser often resets focus to ",[170,687,688],{},"\u003Cbody>"," or the previously focused element, causing severe disorientation for keyboard users. Implement a deterministic focus restoration strategy using React lifecycle hooks to target the first actionable element immediately after mount.",[184,691,265],{"id":692},"implementation-steps-1",[267,694,695,702,709,712,719],{},[192,696,697,698,701],{},"Attach a ",[170,699,700],{},"useRef"," to the component container.",[192,703,704,705,708],{},"Trigger a ",[170,706,707],{},"useEffect"," on mount completion.",[192,710,711],{},"Query the DOM for the first valid interactive element using a standard focusable selector.",[192,713,714,715,718],{},"Call ",[170,716,717],{},".focus({ preventScroll: true })"," to maintain viewport position.",[192,720,721],{},"Implement fallback logic for components that initially render in a disabled or empty state.",[297,723,725],{"className":299,"code":724,"language":301,"meta":302,"style":302},"import { useEffect, useRef } from 'react';\n\nexport function useFocusOnMount() {\n const containerRef = useRef\u003CHTMLDivElement>(null);\n\n useEffect(() => {\n if (containerRef.current) {\n const focusable = containerRef.current.querySelector(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n ) as HTMLElement | null;\n \n if (focusable) {\n focusable.focus({ preventScroll: true });\n }\n }\n }, []);\n\n return containerRef;\n}\n",[170,726,727,741,745,756,784,788,799,807,825,830,849,854,861,877,882,886,891,895,902],{"__ignoreMap":302},[306,728,729,731,734,736,739],{"class":308,"line":309},[306,730,313],{"class":312},[306,732,733],{"class":316}," { useEffect, useRef } ",[306,735,320],{"class":312},[306,737,738],{"class":323}," 'react'",[306,740,327],{"class":316},[306,742,743],{"class":308,"line":330},[306,744,334],{"emptyLinePlaceholder":333},[306,746,747,749,751,754],{"class":308,"line":337},[306,748,477],{"class":312},[306,750,483],{"class":312},[306,752,753],{"class":350}," useFocusOnMount",[306,755,489],{"class":316},[306,757,758,761,764,766,769,772,775,778,781],{"class":308,"line":372},[306,759,760],{"class":312}," const",[306,762,763],{"class":343}," containerRef",[306,765,347],{"class":312},[306,767,768],{"class":350}," useRef",[306,770,771],{"class":316},"\u003C",[306,773,774],{"class":350},"HTMLDivElement",[306,776,777],{"class":316},">(",[306,779,780],{"class":343},"null",[306,782,783],{"class":316},");\n",[306,785,786],{"class":308,"line":386},[306,787,334],{"emptyLinePlaceholder":333},[306,789,790,793,795,797],{"class":308,"line":416},[306,791,792],{"class":350}," useEffect",[306,794,354],{"class":316},[306,796,357],{"class":312},[306,798,603],{"class":316},[306,800,801,804],{"class":308,"line":438},[306,802,803],{"class":312}," if",[306,805,806],{"class":316}," (containerRef.current) {\n",[306,808,809,811,814,816,819,822],{"class":308,"line":448},[306,810,760],{"class":312},[306,812,813],{"class":343}," focusable",[306,815,347],{"class":312},[306,817,818],{"class":316}," containerRef.current.",[306,820,821],{"class":350},"querySelector",[306,823,824],{"class":316},"(\n",[306,826,827],{"class":308,"line":454},[306,828,829],{"class":323}," 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n",[306,831,832,835,838,841,844,847],{"class":308,"line":463},[306,833,834],{"class":316}," ) ",[306,836,837],{"class":312},"as",[306,839,840],{"class":350}," HTMLElement",[306,842,843],{"class":312}," |",[306,845,846],{"class":343}," null",[306,848,327],{"class":316},[306,850,851],{"class":308,"line":469},[306,852,853],{"class":316}," \n",[306,855,856,858],{"class":308,"line":474},[306,857,803],{"class":312},[306,859,860],{"class":316}," (focusable) {\n",[306,862,863,866,869,872,874],{"class":308,"line":492},[306,864,865],{"class":316}," focusable.",[306,867,868],{"class":350},"focus",[306,870,871],{"class":316},"({ preventScroll: ",[306,873,673],{"class":343},[306,875,876],{"class":316}," });\n",[306,878,879],{"class":308,"line":500},[306,880,881],{"class":316}," }\n",[306,883,884],{"class":308,"line":510},[306,885,881],{"class":316},[306,887,888],{"class":308,"line":521},[306,889,890],{"class":316}," }, []);\n",[306,892,893],{"class":308,"line":530},[306,894,334],{"emptyLinePlaceholder":333},[306,896,897,899],{"class":308,"line":536},[306,898,495],{"class":312},[306,900,901],{"class":316}," containerRef;\n",[306,903,905],{"class":308,"line":904},19,[306,906,539],{"class":316},[184,908,543],{"id":909},"debugging-ci-testing-workflow-1",[189,911,912,922,928],{},[192,913,914,917,918,921],{},[195,915,916],{},"Focus Trace:"," Open Chrome DevTools → Elements → Accessibility pane. Monitor ",[170,919,920],{},"activeElement"," during component hydration.",[192,923,924,927],{},[195,925,926],{},"Keyboard-Only Navigation:"," Disable mouse input. Tab through the UI post-load. Verify focus lands predictably on the first actionable element.",[192,929,930,933,934,937,938,941],{},[195,931,932],{},"Automated Validation:"," Use ",[170,935,936],{},"@testing-library\u002Freact"," to simulate mount and assert ",[170,939,940],{},"document.activeElement"," matches the expected interactive node.",[297,943,947],{"className":944,"code":945,"language":946,"meta":302,"style":302},"language-ts shiki shiki-themes github-light github-dark","import { render, screen } from '@testing-library\u002Freact';\nimport { useFocusOnMount } from '.\u002Fhooks';\n\ntest('focuses first interactive element on mount', () => {\nrender(\u003CTestComponent \u002F>);\nexpect(document.activeElement?.tagName).toBe('BUTTON');\n});\n","ts",[170,948,949,963,977,981,998,1012,1030],{"__ignoreMap":302},[306,950,951,953,956,958,961],{"class":308,"line":309},[306,952,313],{"class":312},[306,954,955],{"class":316}," { render, screen } ",[306,957,320],{"class":312},[306,959,960],{"class":323}," '@testing-library\u002Freact'",[306,962,327],{"class":316},[306,964,965,967,970,972,975],{"class":308,"line":330},[306,966,313],{"class":312},[306,968,969],{"class":316}," { useFocusOnMount } ",[306,971,320],{"class":312},[306,973,974],{"class":323}," '.\u002Fhooks'",[306,976,327],{"class":316},[306,978,979],{"class":308,"line":337},[306,980,334],{"emptyLinePlaceholder":333},[306,982,983,986,988,991,994,996],{"class":308,"line":372},[306,984,985],{"class":350},"test",[306,987,363],{"class":316},[306,989,990],{"class":323},"'focuses first interactive element on mount'",[306,992,993],{"class":316},", () ",[306,995,357],{"class":312},[306,997,603],{"class":316},[306,999,1000,1003,1006,1009],{"class":308,"line":386},[306,1001,1002],{"class":350},"render",[306,1004,1005],{"class":316},"(\u003C",[306,1007,1008],{"class":350},"TestComponent",[306,1010,1011],{"class":316}," \u002F>);\n",[306,1013,1014,1017,1020,1023,1025,1028],{"class":308,"line":416},[306,1015,1016],{"class":350},"expect",[306,1018,1019],{"class":316},"(document.activeElement?.tagName).",[306,1021,1022],{"class":350},"toBe",[306,1024,363],{"class":316},[306,1026,1027],{"class":323},"'BUTTON'",[306,1029,783],{"class":316},[306,1031,1032],{"class":308,"line":438},[306,1033,466],{"class":316},[236,1035],{},[239,1037,1039],{"id":1038},"announcing-state-changes-with-aria-live-regions","Announcing State Changes with ARIA Live Regions",[166,1041,1042],{},"Screen readers require explicit notification when asynchronous content finishes rendering. Implement an ARIA live region wrapper to broadcast completion states without hijacking the speech queue or interrupting active user input.",[184,1044,265],{"id":1045},"implementation-steps-2",[267,1047,1048,1051,1057,1064],{},[192,1049,1050],{},"Create a dedicated announcer component isolated from the main layout flow.",[192,1052,277,1053,1056],{},[170,1054,1055],{},"aria-live=\"polite\""," to defer announcements until the user pauses input.",[192,1058,1059,1060,1063],{},"Use ",[170,1061,1062],{},"aria-atomic=\"true\""," only if the entire region updates simultaneously; otherwise, omit to prevent redundant speech.",[192,1065,1066,1067,1070,1071,1073],{},"Conditionally render the component only when ",[170,1068,1069],{},"isComplete"," transitions to ",[170,1072,673],{},", then clear the DOM node to prevent queue overflow.",[297,1075,1077],{"className":299,"code":1076,"language":301,"meta":302,"style":302},"export function LoadAnnouncer({ isComplete, label }: { isComplete: boolean; label: string }) {\n if (!isComplete) return null;\n\n return (\n \u003Cdiv aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {label} has finished loading.\n \u003C\u002Fdiv>\n );\n}\n",[170,1078,1079,1129,1149,1153,1159,1188,1193,1201,1205],{"__ignoreMap":302},[306,1080,1081,1083,1085,1088,1091,1094,1097,1100,1103,1106,1109,1111,1113,1116,1119,1121,1123,1126],{"class":308,"line":309},[306,1082,477],{"class":312},[306,1084,483],{"class":312},[306,1086,1087],{"class":350}," LoadAnnouncer",[306,1089,1090],{"class":316},"({ ",[306,1092,1069],{"class":1093},"s4XuR",[306,1095,1096],{"class":316},", ",[306,1098,1099],{"class":1093},"label",[306,1101,1102],{"class":316}," }",[306,1104,1105],{"class":312},":",[306,1107,1108],{"class":316}," { ",[306,1110,1069],{"class":1093},[306,1112,1105],{"class":312},[306,1114,1115],{"class":343}," boolean",[306,1117,1118],{"class":316},"; ",[306,1120,1099],{"class":1093},[306,1122,1105],{"class":312},[306,1124,1125],{"class":343}," string",[306,1127,1128],{"class":316}," }) {\n",[306,1130,1131,1133,1136,1139,1142,1145,1147],{"class":308,"line":330},[306,1132,803],{"class":312},[306,1134,1135],{"class":316}," (",[306,1137,1138],{"class":312},"!",[306,1140,1141],{"class":316},"isComplete) ",[306,1143,1144],{"class":312},"return",[306,1146,846],{"class":343},[306,1148,327],{"class":316},[306,1150,1151],{"class":308,"line":337},[306,1152,334],{"emptyLinePlaceholder":333},[306,1154,1155,1157],{"class":308,"line":372},[306,1156,495],{"class":312},[306,1158,383],{"class":316},[306,1160,1161,1163,1165,1168,1170,1173,1176,1178,1180,1182,1184,1186],{"class":308,"line":386},[306,1162,389],{"class":316},[306,1164,393],{"class":392},[306,1166,1167],{"class":350}," aria-live",[306,1169,399],{"class":312},[306,1171,1172],{"class":323},"\"polite\"",[306,1174,1175],{"class":350}," aria-atomic",[306,1177,399],{"class":312},[306,1179,402],{"class":323},[306,1181,423],{"class":350},[306,1183,399],{"class":312},[306,1185,428],{"class":323},[306,1187,413],{"class":316},[306,1189,1190],{"class":308,"line":416},[306,1191,1192],{"class":316}," {label} has finished loading.\n",[306,1194,1195,1197,1199],{"class":308,"line":438},[306,1196,441],{"class":316},[306,1198,393],{"class":392},[306,1200,413],{"class":316},[306,1202,1203],{"class":308,"line":448},[306,1204,533],{"class":316},[306,1206,1207],{"class":308,"line":454},[306,1208,539],{"class":316},[184,1210,543],{"id":1211},"debugging-ci-testing-workflow-2",[189,1213,1214,1220,1226],{},[192,1215,1216,1219],{},[195,1217,1218],{},"Speech Queue Audit:"," Use VoiceOver\u002FNVDA to verify the completion message is queued politely. Confirm it does not interrupt ongoing navigation or form entry.",[192,1221,1222,1225],{},[195,1223,1224],{},"DOM Inspection:"," Verify the announcer element is removed from the DOM immediately after the screen reader processes the text.",[192,1227,1228,1231,1232,1235,1236,1239,1240,1243],{},[195,1229,1230],{},"CI Pipeline Enforcement:"," Integrate ",[170,1233,1234],{},"pa11y-ci"," into your deployment pipeline. Configure it to scan staging URLs and fail if ",[170,1237,1238],{},"aria-live"," regions lack ",[170,1241,1242],{},"polite"," attributes or if duplicate live regions exist in the DOM.",[236,1245],{},[239,1247,1249],{"id":1248},"common-implementation-pitfalls","Common Implementation Pitfalls",[189,1251,1252,1262,1271,1281,1291],{},[192,1253,1254,1257,1258,1261],{},[195,1255,1256],{},"Focus Traps in Suspense Fallbacks:"," Applying ",[170,1259,1260],{},"tabindex=\"0\""," to loading skeletons creates artificial keyboard traps. Remove explicit tab indices from non-interactive placeholders.",[192,1263,1264,1267,1268,1270],{},[195,1265,1266],{},"Container Focus Misdirection:"," Focusing the wrapper ",[170,1269,255],{}," instead of the first actionable child breaks WCAG 2.4.3. Always target native interactive elements.",[192,1272,1273,1276,1277,1280],{},[195,1274,1275],{},"Aggressive Live Regions:"," Overusing ",[170,1278,1279],{},"aria-live=\"assertive\""," interrupts ongoing screen reader output. Reserve assertive states for critical errors only.",[192,1282,1283,1286,1287,1290],{},[195,1284,1285],{},"Motion Preference Ignorance:"," Loading skeleton fade transitions must respect ",[170,1288,1289],{},"@media (prefers-reduced-motion: reduce)",". Disable CSS animations for users requiring reduced motion.",[192,1292,1293,1296,1297,1300,1301,1303,1304,1307],{},[195,1294,1295],{},"Unreliable Timing:"," Relying on ",[170,1298,1299],{},"setTimeout"," for focus restoration creates race conditions with React hydration. Always use ",[170,1302,707],{}," or ",[170,1305,1306],{},"MutationObserver"," tied to actual DOM updates.",[236,1309],{},[239,1311,1313],{"id":1312},"frequently-asked-questions","Frequently Asked Questions",[184,1315,1317,1318,1320],{"id":1316},"does-nextdynamic-break-keyboard-navigation-by-default","Does ",[170,1319,172],{}," break keyboard navigation by default?",[166,1322,1323],{},"Not inherently. However, the default loading state lacks semantic structure. Without explicit focus management and ARIA attributes, deferred components cause focus loss or disrupt tab order upon mount.",[184,1325,1327],{"id":1326},"how-do-i-restore-focus-after-a-lazy-loaded-component-finishes-rendering","How do I restore focus after a lazy-loaded component finishes rendering?",[166,1329,1330,1331,1333,1334,1336],{},"Use a ",[170,1332,707],{}," hook that executes after mount. Query the first focusable element inside the component container and invoke ",[170,1335,717],{},". Never focus non-interactive wrappers.",[184,1338,1340,1341,1344],{"id":1339},"should-i-use-aria-liveassertive-for-dynamic-import-loading-states","Should I use ",[170,1342,1343],{},"aria-live='assertive'"," for dynamic import loading states?",[166,1346,1347,1348,1351],{},"No. Always use ",[170,1349,1350],{},"aria-live='polite'"," for loading announcements. Assertive regions interrupt current screen reader output, which severely degrades the user experience during navigation or data entry.",[1353,1354,1355],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":302,"searchDepth":330,"depth":330,"links":1357},[1358,1359,1360,1364,1368,1372,1373],{"id":186,"depth":337,"text":187},{"id":219,"depth":337,"text":220},{"id":241,"depth":330,"text":242,"children":1361},[1362,1363],{"id":264,"depth":337,"text":265},{"id":542,"depth":337,"text":543},{"id":681,"depth":330,"text":682,"children":1365},[1366,1367],{"id":692,"depth":337,"text":265},{"id":909,"depth":337,"text":543},{"id":1038,"depth":330,"text":1039,"children":1369},[1370,1371],{"id":1045,"depth":337,"text":265},{"id":1211,"depth":337,"text":543},{"id":1248,"depth":330,"text":1249},{"id":1312,"depth":330,"text":1313,"children":1374},[1375,1377,1378],{"id":1316,"depth":337,"text":1376},"Does next\u002Fdynamic break keyboard navigation by default?",{"id":1326,"depth":337,"text":1327},{"id":1339,"depth":337,"text":1379},"Should I use aria-live='assertive' for dynamic import loading states?",null,"Use dynamic imports in Next.js without breaking keyboard navigation by preserving focus continuity and announcing async UI updates clearly.","md",{},false,{"title":121,"description":1381},"gJrJJcAciCAu_pPjHyB7LGrmEDzhWMXF-ITT_kiCE3o",[1388,1418,1419],{"title":5,"path":6,"stem":7,"children":1389},[1390,1391,1394,1397,1403,1409,1415],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1392},[1393],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1395},[1396],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1398},[1399,1400],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":1401},[1402],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":1404},[1405,1406],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":1407},[1408],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":1410},[1411,1412],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":1413},[1414],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":1416},[1417],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},{"title":71,"path":72,"stem":73,"children":1420},[1421,1422,1428,1434,1437,1446,1455],{"title":76,"path":72,"stem":77},{"title":79,"path":80,"stem":81,"children":1423},[1424,1425],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":1426},[1427],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":1429},[1430,1431],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":1432},[1433],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":1435},[1436],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":1438},[1439,1440,1443],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":1441},[1442],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":1444},[1445],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":1447},[1448,1449,1452],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":1450},[1451],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":1453},[1454],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":1456},[1457,1458],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":1459},[1460],{"title":151,"path":152,"stem":153},1778094796242]