[{"data":1,"prerenderedAt":2244},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002F":156,"content-navigation":2170},[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":151,"body":158,"date":2163,"description":2164,"extension":2165,"image":2163,"meta":2166,"modifiedAt":2163,"navigation":360,"noindex":2167,"path":152,"publishedAt":2163,"seo":2168,"stem":153,"updatedAt":2163,"__hash__":2169},"content\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002Findex.md",{"type":159,"value":160,"toc":2148},"minimark",[161,165,178,184,216,221,247,252,255,273,279,283,286,313,326,334,1436,1440,1443,1474,1484,1491,1962,1966,1969,1995,2000,2004,2007,2025,2030,2034,2064,2068,2093,2114,2131,2145],[162,163,151],"h1",{"id":164},"handling-accessible-modals-in-nextjs-14-server-components",[166,167,168,169,173,174,177],"p",{},"Building accessible modal dialogs in Next.js 14 requires a deliberate architectural split between static markup and interactive behavior. Because Server Components cannot attach event listeners or manage DOM focus, developers must isolate interactive logic within Client Components while preserving the performance benefits of established ",[170,171,76],"a",{"href":172},"\u002Freact-nextjs-accessibility-patterns\u002F",". This guide provides a reproducible, single-intent implementation for handling modal state, focus trapping, and screen reader announcements, ensuring compliance with modern web standards while navigating the ",[170,175,145],{"href":176},"\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002F"," boundary.",[166,179,180],{},[181,182,183],"strong",{},"Target WCAG 2.2 Criteria",[185,186,187,195,200,205,210],"ul",{},[188,189,190,194],"li",{},[191,192,193],"code",{},"2.1.1 Keyboard"," (Level A)",[188,196,197,194],{},[191,198,199],{},"2.4.3 Focus Order",[188,201,202,194],{},[191,203,204],{},"4.1.2 Name, Role, Value",[188,206,207,194],{},[191,208,209],{},"1.3.1 Info and Relationships",[188,211,212,215],{},[191,213,214],{},"1.4.13 Content on Hover or Focus"," (Level AA)",[166,217,218],{},[181,219,220],{},"Implementation Key Points",[185,222,223,230,233,244],{},[188,224,225,226,229],{},"Isolate interactive modal logic to a ",[191,227,228],{},"'use client'"," boundary",[188,231,232],{},"Implement programmatic focus trapping and return-focus restoration",[188,234,235,236,239,240,243],{},"Apply strict ",[191,237,238],{},"role=\"dialog\""," and ",[191,241,242],{},"aria-modal=\"true\""," attributes",[188,245,246],{},"Synchronize open\u002Fclose state with live regions for screen readers",[248,249,251],"h2",{"id":250},"_1-architectural-boundary-server-markup-vs-client-interactivity","1. Architectural Boundary: Server Markup vs Client Interactivity",[166,253,254],{},"Define the structural split required to render modal triggers server-side while delegating focus management and keyboard events to a Client Component.",[185,256,257,260,267,270],{},[188,258,259],{},"Server Components render initial DOM and trigger buttons",[188,261,262,263,266],{},"Client Component handles ",[191,264,265],{},"isOpen"," state, event listeners, and focus logic",[188,268,269],{},"Props drilling must be minimized to preserve hydration performance",[188,271,272],{},"Use React Context or custom state hooks for cross-component communication",[166,274,275,278],{},[181,276,277],{},"Testing Note:"," Verify that the initial page load contains zero client-side JavaScript for the modal trigger until user interaction occurs.",[248,280,282],{"id":281},"_2-implementing-the-accessible-modal-shell","2. Implementing the Accessible Modal Shell",[166,284,285],{},"Construct the base dialog component with mandatory ARIA attributes, backdrop overlay, and keyboard dismissal handlers.",[185,287,288,296,303,310],{},[188,289,290,291,239,293,295],{},"Apply ",[191,292,238],{},[191,294,242],{}," to the container",[188,297,298,299,302],{},"Attach ",[191,300,301],{},"aria-labelledby"," pointing to the modal title",[188,304,305,306,309],{},"Implement ",[191,307,308],{},"Escape"," key listener for programmatic closure",[188,311,312],{},"Render backdrop with inert behavior to prevent background interaction",[166,314,315,317,318,321,322,325],{},[181,316,277],{}," Run ",[191,319,320],{},"axe-core"," to confirm ",[191,323,324],{},"aria-modal"," is correctly scoped and no background elements receive focus.",[327,328,330,333],"h3",{"id":329},"accessiblemodaltsx-client-component",[191,331,332],{},"AccessibleModal.tsx"," (Client Component)",[335,336,341],"pre",{"className":337,"code":338,"language":339,"meta":340,"style":340},"language-tsx shiki shiki-themes github-light github-dark","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useFocusTrap } from '.\u002FuseFocusTrap';\n\ninterface AccessibleModalProps {\n isOpen: boolean;\n onClose: () => void;\n titleId: string;\n children: React.ReactNode;\n}\n\nexport function AccessibleModal({ isOpen, onClose, titleId, children }: AccessibleModalProps) {\n const dialogRef = useRef\u003CHTMLDivElement>(null);\n const [isMounted, setIsMounted] = useState(false);\n\n useFocusTrap(dialogRef, isOpen);\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n useEffect(() => {\n if (isOpen) {\n document.body.style.overflow = 'hidden';\n } else {\n document.body.style.overflow = '';\n }\n return () => { document.body.style.overflow = ''; };\n }, [isOpen]);\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === 'Escape') onClose();\n };\n\n const handleBackdropClick = (e: React.MouseEvent\u003CHTMLDivElement>) => {\n if (e.target === e.currentTarget) onClose();\n };\n\n if (!isOpen || !isMounted) return null;\n\n return createPortal(\n \u003Cdiv\n className=\"modal-backdrop\"\n onClick={handleBackdropClick}\n onKeyDown={handleKeyDown}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n style={{\n position: 'fixed',\n inset: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 50\n }}\n >\n \u003Cdiv\n ref={dialogRef}\n tabIndex={-1}\n style={{\n background: '#fff',\n padding: '2rem',\n borderRadius: '8px',\n maxWidth: '90vw',\n maxHeight: '90vh',\n overflowY: 'auto',\n outline: 'none'\n }}\n >\n \u003Cdiv style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>\n \u003Ch2 id={titleId} style={{ margin: 0 }}>Modal Title\u003C\u002Fh2>\n \u003Cbutton\n onClick={onClose}\n aria-label=\"Close dialog\"\n style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.5rem' }}\n >\n &times;\n \u003C\u002Fbutton>\n \u003C\u002Fdiv>\n {children}\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>,\n document.body\n );\n}\n","tsx","",[191,342,343,355,362,380,395,410,415,428,444,463,476,495,501,506,549,579,612,617,626,631,644,657,663,668,679,688,701,712,724,730,750,756,761,793,814,820,825,859,876,881,886,916,921,932,942,953,964,975,986,997,1008,1019,1031,1042,1053,1064,1075,1085,1094,1100,1106,1113,1124,1143,1152,1163,1174,1185,1196,1207,1218,1227,1232,1237,1269,1302,1310,1320,1331,1363,1368,1374,1385,1394,1400,1409,1419,1425,1431],{"__ignoreMap":340},[344,345,348,351],"span",{"class":346,"line":347},"line",1,[344,349,228],{"class":350},"sZZnC",[344,352,354],{"class":353},"sVt8B",";\n",[344,356,358],{"class":346,"line":357},2,[344,359,361],{"emptyLinePlaceholder":360},true,"\n",[344,363,365,369,372,375,378],{"class":346,"line":364},3,[344,366,368],{"class":367},"szBVR","import",[344,370,371],{"class":353}," { useEffect, useRef, useState } ",[344,373,374],{"class":367},"from",[344,376,377],{"class":350}," 'react'",[344,379,354],{"class":353},[344,381,383,385,388,390,393],{"class":346,"line":382},4,[344,384,368],{"class":367},[344,386,387],{"class":353}," { createPortal } ",[344,389,374],{"class":367},[344,391,392],{"class":350}," 'react-dom'",[344,394,354],{"class":353},[344,396,398,400,403,405,408],{"class":346,"line":397},5,[344,399,368],{"class":367},[344,401,402],{"class":353}," { useFocusTrap } ",[344,404,374],{"class":367},[344,406,407],{"class":350}," '.\u002FuseFocusTrap'",[344,409,354],{"class":353},[344,411,413],{"class":346,"line":412},6,[344,414,361],{"emptyLinePlaceholder":360},[344,416,418,421,425],{"class":346,"line":417},7,[344,419,420],{"class":367},"interface",[344,422,424],{"class":423},"sScJk"," AccessibleModalProps",[344,426,427],{"class":353}," {\n",[344,429,431,435,438,442],{"class":346,"line":430},8,[344,432,434],{"class":433},"s4XuR"," isOpen",[344,436,437],{"class":367},":",[344,439,441],{"class":440},"sj4cs"," boolean",[344,443,354],{"class":353},[344,445,447,450,452,455,458,461],{"class":346,"line":446},9,[344,448,449],{"class":423}," onClose",[344,451,437],{"class":367},[344,453,454],{"class":353}," () ",[344,456,457],{"class":367},"=>",[344,459,460],{"class":440}," void",[344,462,354],{"class":353},[344,464,466,469,471,474],{"class":346,"line":465},10,[344,467,468],{"class":433}," titleId",[344,470,437],{"class":367},[344,472,473],{"class":440}," string",[344,475,354],{"class":353},[344,477,479,482,484,487,490,493],{"class":346,"line":478},11,[344,480,481],{"class":433}," children",[344,483,437],{"class":367},[344,485,486],{"class":423}," React",[344,488,489],{"class":353},".",[344,491,492],{"class":423},"ReactNode",[344,494,354],{"class":353},[344,496,498],{"class":346,"line":497},12,[344,499,500],{"class":353},"}\n",[344,502,504],{"class":346,"line":503},13,[344,505,361],{"emptyLinePlaceholder":360},[344,507,509,512,515,518,521,523,526,529,531,534,536,539,542,544,546],{"class":346,"line":508},14,[344,510,511],{"class":367},"export",[344,513,514],{"class":367}," function",[344,516,517],{"class":423}," AccessibleModal",[344,519,520],{"class":353},"({ ",[344,522,265],{"class":433},[344,524,525],{"class":353},", ",[344,527,528],{"class":433},"onClose",[344,530,525],{"class":353},[344,532,533],{"class":433},"titleId",[344,535,525],{"class":353},[344,537,538],{"class":433},"children",[344,540,541],{"class":353}," }",[344,543,437],{"class":367},[344,545,424],{"class":423},[344,547,548],{"class":353},") {\n",[344,550,552,555,558,561,564,567,570,573,576],{"class":346,"line":551},15,[344,553,554],{"class":367}," const",[344,556,557],{"class":440}," dialogRef",[344,559,560],{"class":367}," =",[344,562,563],{"class":423}," useRef",[344,565,566],{"class":353},"\u003C",[344,568,569],{"class":423},"HTMLDivElement",[344,571,572],{"class":353},">(",[344,574,575],{"class":440},"null",[344,577,578],{"class":353},");\n",[344,580,582,584,587,590,592,595,598,601,604,607,610],{"class":346,"line":581},16,[344,583,554],{"class":367},[344,585,586],{"class":353}," [",[344,588,589],{"class":440},"isMounted",[344,591,525],{"class":353},[344,593,594],{"class":440},"setIsMounted",[344,596,597],{"class":353},"] ",[344,599,600],{"class":367},"=",[344,602,603],{"class":423}," useState",[344,605,606],{"class":353},"(",[344,608,609],{"class":440},"false",[344,611,578],{"class":353},[344,613,615],{"class":346,"line":614},17,[344,616,361],{"emptyLinePlaceholder":360},[344,618,620,623],{"class":346,"line":619},18,[344,621,622],{"class":423}," useFocusTrap",[344,624,625],{"class":353},"(dialogRef, isOpen);\n",[344,627,629],{"class":346,"line":628},19,[344,630,361],{"emptyLinePlaceholder":360},[344,632,634,637,640,642],{"class":346,"line":633},20,[344,635,636],{"class":423}," useEffect",[344,638,639],{"class":353},"(() ",[344,641,457],{"class":367},[344,643,427],{"class":353},[344,645,647,650,652,655],{"class":346,"line":646},21,[344,648,649],{"class":423}," setIsMounted",[344,651,606],{"class":353},[344,653,654],{"class":440},"true",[344,656,578],{"class":353},[344,658,660],{"class":346,"line":659},22,[344,661,662],{"class":353}," }, []);\n",[344,664,666],{"class":346,"line":665},23,[344,667,361],{"emptyLinePlaceholder":360},[344,669,671,673,675,677],{"class":346,"line":670},24,[344,672,636],{"class":423},[344,674,639],{"class":353},[344,676,457],{"class":367},[344,678,427],{"class":353},[344,680,682,685],{"class":346,"line":681},25,[344,683,684],{"class":367}," if",[344,686,687],{"class":353}," (isOpen) {\n",[344,689,691,694,696,699],{"class":346,"line":690},26,[344,692,693],{"class":353}," document.body.style.overflow ",[344,695,600],{"class":367},[344,697,698],{"class":350}," 'hidden'",[344,700,354],{"class":353},[344,702,704,707,710],{"class":346,"line":703},27,[344,705,706],{"class":353}," } ",[344,708,709],{"class":367},"else",[344,711,427],{"class":353},[344,713,715,717,719,722],{"class":346,"line":714},28,[344,716,693],{"class":353},[344,718,600],{"class":367},[344,720,721],{"class":350}," ''",[344,723,354],{"class":353},[344,725,727],{"class":346,"line":726},29,[344,728,729],{"class":353}," }\n",[344,731,733,736,738,740,743,745,747],{"class":346,"line":732},30,[344,734,735],{"class":367}," return",[344,737,454],{"class":353},[344,739,457],{"class":367},[344,741,742],{"class":353}," { document.body.style.overflow ",[344,744,600],{"class":367},[344,746,721],{"class":350},[344,748,749],{"class":353},"; };\n",[344,751,753],{"class":346,"line":752},31,[344,754,755],{"class":353}," }, [isOpen]);\n",[344,757,759],{"class":346,"line":758},32,[344,760,361],{"emptyLinePlaceholder":360},[344,762,764,766,769,771,774,777,779,781,783,786,789,791],{"class":346,"line":763},33,[344,765,554],{"class":367},[344,767,768],{"class":423}," handleKeyDown",[344,770,560],{"class":367},[344,772,773],{"class":353}," (",[344,775,776],{"class":433},"e",[344,778,437],{"class":367},[344,780,486],{"class":423},[344,782,489],{"class":353},[344,784,785],{"class":423},"KeyboardEvent",[344,787,788],{"class":353},") ",[344,790,457],{"class":367},[344,792,427],{"class":353},[344,794,796,798,801,804,807,809,811],{"class":346,"line":795},34,[344,797,684],{"class":367},[344,799,800],{"class":353}," (e.key ",[344,802,803],{"class":367},"===",[344,805,806],{"class":350}," 'Escape'",[344,808,788],{"class":353},[344,810,528],{"class":423},[344,812,813],{"class":353},"();\n",[344,815,817],{"class":346,"line":816},35,[344,818,819],{"class":353}," };\n",[344,821,823],{"class":346,"line":822},36,[344,824,361],{"emptyLinePlaceholder":360},[344,826,828,830,833,835,837,839,841,843,845,848,850,852,855,857],{"class":346,"line":827},37,[344,829,554],{"class":367},[344,831,832],{"class":423}," handleBackdropClick",[344,834,560],{"class":367},[344,836,773],{"class":353},[344,838,776],{"class":433},[344,840,437],{"class":367},[344,842,486],{"class":423},[344,844,489],{"class":353},[344,846,847],{"class":423},"MouseEvent",[344,849,566],{"class":353},[344,851,569],{"class":423},[344,853,854],{"class":353},">) ",[344,856,457],{"class":367},[344,858,427],{"class":353},[344,860,862,864,867,869,872,874],{"class":346,"line":861},38,[344,863,684],{"class":367},[344,865,866],{"class":353}," (e.target ",[344,868,803],{"class":367},[344,870,871],{"class":353}," e.currentTarget) ",[344,873,528],{"class":423},[344,875,813],{"class":353},[344,877,879],{"class":346,"line":878},39,[344,880,819],{"class":353},[344,882,884],{"class":346,"line":883},40,[344,885,361],{"emptyLinePlaceholder":360},[344,887,889,891,893,896,899,902,905,908,911,914],{"class":346,"line":888},41,[344,890,684],{"class":367},[344,892,773],{"class":353},[344,894,895],{"class":367},"!",[344,897,898],{"class":353},"isOpen ",[344,900,901],{"class":367},"||",[344,903,904],{"class":367}," !",[344,906,907],{"class":353},"isMounted) ",[344,909,910],{"class":367},"return",[344,912,913],{"class":440}," null",[344,915,354],{"class":353},[344,917,919],{"class":346,"line":918},42,[344,920,361],{"emptyLinePlaceholder":360},[344,922,924,926,929],{"class":346,"line":923},43,[344,925,735],{"class":367},[344,927,928],{"class":423}," createPortal",[344,930,931],{"class":353},"(\n",[344,933,935,938],{"class":346,"line":934},44,[344,936,937],{"class":353}," \u003C",[344,939,941],{"class":940},"s9eBZ","div\n",[344,943,945,948,950],{"class":346,"line":944},45,[344,946,947],{"class":423}," className",[344,949,600],{"class":367},[344,951,952],{"class":350},"\"modal-backdrop\"\n",[344,954,956,959,961],{"class":346,"line":955},46,[344,957,958],{"class":423}," onClick",[344,960,600],{"class":367},[344,962,963],{"class":353},"{handleBackdropClick}\n",[344,965,967,970,972],{"class":346,"line":966},47,[344,968,969],{"class":423}," onKeyDown",[344,971,600],{"class":367},[344,973,974],{"class":353},"{handleKeyDown}\n",[344,976,978,981,983],{"class":346,"line":977},48,[344,979,980],{"class":423}," role",[344,982,600],{"class":367},[344,984,985],{"class":350},"\"dialog\"\n",[344,987,989,992,994],{"class":346,"line":988},49,[344,990,991],{"class":423}," aria-modal",[344,993,600],{"class":367},[344,995,996],{"class":350},"\"true\"\n",[344,998,1000,1003,1005],{"class":346,"line":999},50,[344,1001,1002],{"class":423}," aria-labelledby",[344,1004,600],{"class":367},[344,1006,1007],{"class":353},"{titleId}\n",[344,1009,1011,1014,1016],{"class":346,"line":1010},51,[344,1012,1013],{"class":423}," style",[344,1015,600],{"class":367},[344,1017,1018],{"class":353},"{{\n",[344,1020,1022,1025,1028],{"class":346,"line":1021},52,[344,1023,1024],{"class":353}," position: ",[344,1026,1027],{"class":350},"'fixed'",[344,1029,1030],{"class":353},",\n",[344,1032,1034,1037,1040],{"class":346,"line":1033},53,[344,1035,1036],{"class":353}," inset: ",[344,1038,1039],{"class":440},"0",[344,1041,1030],{"class":353},[344,1043,1045,1048,1051],{"class":346,"line":1044},54,[344,1046,1047],{"class":353}," backgroundColor: ",[344,1049,1050],{"class":350},"'rgba(0,0,0,0.5)'",[344,1052,1030],{"class":353},[344,1054,1056,1059,1062],{"class":346,"line":1055},55,[344,1057,1058],{"class":353}," display: ",[344,1060,1061],{"class":350},"'flex'",[344,1063,1030],{"class":353},[344,1065,1067,1070,1073],{"class":346,"line":1066},56,[344,1068,1069],{"class":353}," alignItems: ",[344,1071,1072],{"class":350},"'center'",[344,1074,1030],{"class":353},[344,1076,1078,1081,1083],{"class":346,"line":1077},57,[344,1079,1080],{"class":353}," justifyContent: ",[344,1082,1072],{"class":350},[344,1084,1030],{"class":353},[344,1086,1088,1091],{"class":346,"line":1087},58,[344,1089,1090],{"class":353}," zIndex: ",[344,1092,1093],{"class":440},"50\n",[344,1095,1097],{"class":346,"line":1096},59,[344,1098,1099],{"class":353}," }}\n",[344,1101,1103],{"class":346,"line":1102},60,[344,1104,1105],{"class":353}," >\n",[344,1107,1109,1111],{"class":346,"line":1108},61,[344,1110,937],{"class":353},[344,1112,941],{"class":940},[344,1114,1116,1119,1121],{"class":346,"line":1115},62,[344,1117,1118],{"class":423}," ref",[344,1120,600],{"class":367},[344,1122,1123],{"class":353},"{dialogRef}\n",[344,1125,1127,1130,1132,1135,1138,1141],{"class":346,"line":1126},63,[344,1128,1129],{"class":423}," tabIndex",[344,1131,600],{"class":367},[344,1133,1134],{"class":353},"{",[344,1136,1137],{"class":367},"-",[344,1139,1140],{"class":440},"1",[344,1142,500],{"class":353},[344,1144,1146,1148,1150],{"class":346,"line":1145},64,[344,1147,1013],{"class":423},[344,1149,600],{"class":367},[344,1151,1018],{"class":353},[344,1153,1155,1158,1161],{"class":346,"line":1154},65,[344,1156,1157],{"class":353}," background: ",[344,1159,1160],{"class":350},"'#fff'",[344,1162,1030],{"class":353},[344,1164,1166,1169,1172],{"class":346,"line":1165},66,[344,1167,1168],{"class":353}," padding: ",[344,1170,1171],{"class":350},"'2rem'",[344,1173,1030],{"class":353},[344,1175,1177,1180,1183],{"class":346,"line":1176},67,[344,1178,1179],{"class":353}," borderRadius: ",[344,1181,1182],{"class":350},"'8px'",[344,1184,1030],{"class":353},[344,1186,1188,1191,1194],{"class":346,"line":1187},68,[344,1189,1190],{"class":353}," maxWidth: ",[344,1192,1193],{"class":350},"'90vw'",[344,1195,1030],{"class":353},[344,1197,1199,1202,1205],{"class":346,"line":1198},69,[344,1200,1201],{"class":353}," maxHeight: ",[344,1203,1204],{"class":350},"'90vh'",[344,1206,1030],{"class":353},[344,1208,1210,1213,1216],{"class":346,"line":1209},70,[344,1211,1212],{"class":353}," overflowY: ",[344,1214,1215],{"class":350},"'auto'",[344,1217,1030],{"class":353},[344,1219,1221,1224],{"class":346,"line":1220},71,[344,1222,1223],{"class":353}," outline: ",[344,1225,1226],{"class":350},"'none'\n",[344,1228,1230],{"class":346,"line":1229},72,[344,1231,1099],{"class":353},[344,1233,1235],{"class":346,"line":1234},73,[344,1236,1105],{"class":353},[344,1238,1240,1242,1245,1247,1249,1252,1254,1257,1260,1263,1266],{"class":346,"line":1239},74,[344,1241,937],{"class":353},[344,1243,1244],{"class":940},"div",[344,1246,1013],{"class":423},[344,1248,600],{"class":367},[344,1250,1251],{"class":353},"{{ display: ",[344,1253,1061],{"class":350},[344,1255,1256],{"class":353},", justifyContent: ",[344,1258,1259],{"class":350},"'space-between'",[344,1261,1262],{"class":353},", marginBottom: ",[344,1264,1265],{"class":350},"'1rem'",[344,1267,1268],{"class":353}," }}>\n",[344,1270,1272,1274,1276,1279,1281,1284,1287,1289,1292,1294,1297,1299],{"class":346,"line":1271},75,[344,1273,937],{"class":353},[344,1275,248],{"class":940},[344,1277,1278],{"class":423}," id",[344,1280,600],{"class":367},[344,1282,1283],{"class":353},"{titleId} ",[344,1285,1286],{"class":423},"style",[344,1288,600],{"class":367},[344,1290,1291],{"class":353},"{{ margin: ",[344,1293,1039],{"class":440},[344,1295,1296],{"class":353}," }}>Modal Title\u003C\u002F",[344,1298,248],{"class":940},[344,1300,1301],{"class":353},">\n",[344,1303,1305,1307],{"class":346,"line":1304},76,[344,1306,937],{"class":353},[344,1308,1309],{"class":940},"button\n",[344,1311,1313,1315,1317],{"class":346,"line":1312},77,[344,1314,958],{"class":423},[344,1316,600],{"class":367},[344,1318,1319],{"class":353},"{onClose}\n",[344,1321,1323,1326,1328],{"class":346,"line":1322},78,[344,1324,1325],{"class":423}," aria-label",[344,1327,600],{"class":367},[344,1329,1330],{"class":350},"\"Close dialog\"\n",[344,1332,1334,1336,1338,1341,1344,1347,1349,1352,1355,1358,1361],{"class":346,"line":1333},79,[344,1335,1013],{"class":423},[344,1337,600],{"class":367},[344,1339,1340],{"class":353},"{{ background: ",[344,1342,1343],{"class":350},"'none'",[344,1345,1346],{"class":353},", border: ",[344,1348,1343],{"class":350},[344,1350,1351],{"class":353},", cursor: ",[344,1353,1354],{"class":350},"'pointer'",[344,1356,1357],{"class":353},", fontSize: ",[344,1359,1360],{"class":350},"'1.5rem'",[344,1362,1099],{"class":353},[344,1364,1366],{"class":346,"line":1365},80,[344,1367,1105],{"class":353},[344,1369,1371],{"class":346,"line":1370},81,[344,1372,1373],{"class":440}," &times;\n",[344,1375,1377,1380,1383],{"class":346,"line":1376},82,[344,1378,1379],{"class":353}," \u003C\u002F",[344,1381,1382],{"class":940},"button",[344,1384,1301],{"class":353},[344,1386,1388,1390,1392],{"class":346,"line":1387},83,[344,1389,1379],{"class":353},[344,1391,1244],{"class":940},[344,1393,1301],{"class":353},[344,1395,1397],{"class":346,"line":1396},84,[344,1398,1399],{"class":353}," {children}\n",[344,1401,1403,1405,1407],{"class":346,"line":1402},85,[344,1404,1379],{"class":353},[344,1406,1244],{"class":940},[344,1408,1301],{"class":353},[344,1410,1412,1414,1416],{"class":346,"line":1411},86,[344,1413,1379],{"class":353},[344,1415,1244],{"class":940},[344,1417,1418],{"class":353},">,\n",[344,1420,1422],{"class":346,"line":1421},87,[344,1423,1424],{"class":353}," document.body\n",[344,1426,1428],{"class":346,"line":1427},88,[344,1429,1430],{"class":353}," );\n",[344,1432,1434],{"class":346,"line":1433},89,[344,1435,500],{"class":353},[248,1437,1439],{"id":1438},"_3-focus-trapping-return-focus-restoration","3. Focus Trapping & Return Focus Restoration",[166,1441,1442],{},"Prevent keyboard focus from escaping the modal and restore it to the originating trigger upon closure.",[185,1444,1445,1452,1461,1471],{},[188,1446,1447,1448,1451],{},"Capture the ",[191,1449,1450],{},"document.activeElement"," reference before opening",[188,1453,1454,1455,239,1458],{},"Implement a cyclic focus trap using ",[191,1456,1457],{},"useEffect",[191,1459,1460],{},"keydown",[188,1462,1463,1464,239,1467,1470],{},"Handle ",[191,1465,1466],{},"Tab",[191,1468,1469],{},"Shift+Tab"," boundary conditions",[188,1472,1473],{},"Restore focus synchronously after React state updates and DOM removal",[166,1475,1476,1478,1479,68,1481,1483],{},[181,1477,277],{}," Test with full keyboard navigation (",[191,1480,1466],{},[191,1482,1469],{},") and verify focus remains strictly within the modal until closed.",[327,1485,1487,1490],{"id":1486},"usefocustrapts-custom-hook",[191,1488,1489],{},"useFocusTrap.ts"," (Custom Hook)",[335,1492,1494],{"className":337,"code":1493,"language":339,"meta":340,"style":340},"import { useEffect, useRef } from 'react';\n\nexport function useFocusTrap(containerRef: React.RefObject\u003CHTMLElement | null>, isActive: boolean) {\n const previousFocusRef = useRef\u003CHTMLElement | null>(null);\n\n useEffect(() => {\n if (!isActive || !containerRef.current) return;\n\n previousFocusRef.current = document.activeElement as HTMLElement;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key !== 'Tab') return;\n\n const focusableElements = containerRef.current!.querySelectorAll\u003CHTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n );\n if (focusableElements.length === 0) return;\n\n const firstFocusable = focusableElements[0];\n const lastFocusable = focusableElements[focusableElements.length - 1];\n\n if (e.shiftKey) {\n if (document.activeElement === firstFocusable) {\n e.preventDefault();\n lastFocusable.focus();\n }\n } else {\n if (document.activeElement === lastFocusable) {\n e.preventDefault();\n firstFocusable.focus();\n }\n }\n };\n\n containerRef.current.addEventListener('keydown', handleKeyDown);\n containerRef.current.focus();\n\n return () => {\n containerRef.current?.removeEventListener('keydown', handleKeyDown);\n previousFocusRef.current?.focus();\n };\n }, [isActive, containerRef]);\n}\n",[191,1495,1496,1509,1513,1557,1582,1586,1596,1618,1622,1640,1644,1667,1685,1689,1715,1720,1724,1746,1750,1767,1789,1793,1800,1812,1822,1832,1836,1844,1855,1863,1872,1876,1880,1884,1888,1904,1912,1916,1926,1940,1949,1953,1958],{"__ignoreMap":340},[344,1497,1498,1500,1503,1505,1507],{"class":346,"line":347},[344,1499,368],{"class":367},[344,1501,1502],{"class":353}," { useEffect, useRef } ",[344,1504,374],{"class":367},[344,1506,377],{"class":350},[344,1508,354],{"class":353},[344,1510,1511],{"class":346,"line":357},[344,1512,361],{"emptyLinePlaceholder":360},[344,1514,1515,1517,1519,1521,1523,1526,1528,1530,1532,1535,1537,1540,1543,1545,1548,1551,1553,1555],{"class":346,"line":364},[344,1516,511],{"class":367},[344,1518,514],{"class":367},[344,1520,622],{"class":423},[344,1522,606],{"class":353},[344,1524,1525],{"class":433},"containerRef",[344,1527,437],{"class":367},[344,1529,486],{"class":423},[344,1531,489],{"class":353},[344,1533,1534],{"class":423},"RefObject",[344,1536,566],{"class":353},[344,1538,1539],{"class":423},"HTMLElement",[344,1541,1542],{"class":367}," |",[344,1544,913],{"class":440},[344,1546,1547],{"class":353},">, ",[344,1549,1550],{"class":433},"isActive",[344,1552,437],{"class":367},[344,1554,441],{"class":440},[344,1556,548],{"class":353},[344,1558,1559,1561,1564,1566,1568,1570,1572,1574,1576,1578,1580],{"class":346,"line":382},[344,1560,554],{"class":367},[344,1562,1563],{"class":440}," previousFocusRef",[344,1565,560],{"class":367},[344,1567,563],{"class":423},[344,1569,566],{"class":353},[344,1571,1539],{"class":423},[344,1573,1542],{"class":367},[344,1575,913],{"class":440},[344,1577,572],{"class":353},[344,1579,575],{"class":440},[344,1581,578],{"class":353},[344,1583,1584],{"class":346,"line":397},[344,1585,361],{"emptyLinePlaceholder":360},[344,1587,1588,1590,1592,1594],{"class":346,"line":412},[344,1589,636],{"class":423},[344,1591,639],{"class":353},[344,1593,457],{"class":367},[344,1595,427],{"class":353},[344,1597,1598,1600,1602,1604,1607,1609,1611,1614,1616],{"class":346,"line":417},[344,1599,684],{"class":367},[344,1601,773],{"class":353},[344,1603,895],{"class":367},[344,1605,1606],{"class":353},"isActive ",[344,1608,901],{"class":367},[344,1610,904],{"class":367},[344,1612,1613],{"class":353},"containerRef.current) ",[344,1615,910],{"class":367},[344,1617,354],{"class":353},[344,1619,1620],{"class":346,"line":430},[344,1621,361],{"emptyLinePlaceholder":360},[344,1623,1624,1627,1629,1632,1635,1638],{"class":346,"line":446},[344,1625,1626],{"class":353}," previousFocusRef.current ",[344,1628,600],{"class":367},[344,1630,1631],{"class":353}," document.activeElement ",[344,1633,1634],{"class":367},"as",[344,1636,1637],{"class":423}," HTMLElement",[344,1639,354],{"class":353},[344,1641,1642],{"class":346,"line":465},[344,1643,361],{"emptyLinePlaceholder":360},[344,1645,1646,1648,1650,1652,1654,1656,1658,1661,1663,1665],{"class":346,"line":478},[344,1647,554],{"class":367},[344,1649,768],{"class":423},[344,1651,560],{"class":367},[344,1653,773],{"class":353},[344,1655,776],{"class":433},[344,1657,437],{"class":367},[344,1659,1660],{"class":423}," KeyboardEvent",[344,1662,788],{"class":353},[344,1664,457],{"class":367},[344,1666,427],{"class":353},[344,1668,1669,1671,1673,1676,1679,1681,1683],{"class":346,"line":497},[344,1670,684],{"class":367},[344,1672,800],{"class":353},[344,1674,1675],{"class":367},"!==",[344,1677,1678],{"class":350}," 'Tab'",[344,1680,788],{"class":353},[344,1682,910],{"class":367},[344,1684,354],{"class":353},[344,1686,1687],{"class":346,"line":503},[344,1688,361],{"emptyLinePlaceholder":360},[344,1690,1691,1693,1696,1698,1701,1703,1705,1708,1710,1712],{"class":346,"line":508},[344,1692,554],{"class":367},[344,1694,1695],{"class":440}," focusableElements",[344,1697,560],{"class":367},[344,1699,1700],{"class":353}," containerRef.current",[344,1702,895],{"class":367},[344,1704,489],{"class":353},[344,1706,1707],{"class":423},"querySelectorAll",[344,1709,566],{"class":353},[344,1711,1539],{"class":423},[344,1713,1714],{"class":353},">(\n",[344,1716,1717],{"class":346,"line":551},[344,1718,1719],{"class":350}," 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n",[344,1721,1722],{"class":346,"line":581},[344,1723,1430],{"class":353},[344,1725,1726,1728,1731,1734,1737,1740,1742,1744],{"class":346,"line":614},[344,1727,684],{"class":367},[344,1729,1730],{"class":353}," (focusableElements.",[344,1732,1733],{"class":440},"length",[344,1735,1736],{"class":367}," ===",[344,1738,1739],{"class":440}," 0",[344,1741,788],{"class":353},[344,1743,910],{"class":367},[344,1745,354],{"class":353},[344,1747,1748],{"class":346,"line":619},[344,1749,361],{"emptyLinePlaceholder":360},[344,1751,1752,1754,1757,1759,1762,1764],{"class":346,"line":628},[344,1753,554],{"class":367},[344,1755,1756],{"class":440}," firstFocusable",[344,1758,560],{"class":367},[344,1760,1761],{"class":353}," focusableElements[",[344,1763,1039],{"class":440},[344,1765,1766],{"class":353},"];\n",[344,1768,1769,1771,1774,1776,1779,1781,1784,1787],{"class":346,"line":633},[344,1770,554],{"class":367},[344,1772,1773],{"class":440}," lastFocusable",[344,1775,560],{"class":367},[344,1777,1778],{"class":353}," focusableElements[focusableElements.",[344,1780,1733],{"class":440},[344,1782,1783],{"class":367}," -",[344,1785,1786],{"class":440}," 1",[344,1788,1766],{"class":353},[344,1790,1791],{"class":346,"line":646},[344,1792,361],{"emptyLinePlaceholder":360},[344,1794,1795,1797],{"class":346,"line":659},[344,1796,684],{"class":367},[344,1798,1799],{"class":353}," (e.shiftKey) {\n",[344,1801,1802,1804,1807,1809],{"class":346,"line":665},[344,1803,684],{"class":367},[344,1805,1806],{"class":353}," (document.activeElement ",[344,1808,803],{"class":367},[344,1810,1811],{"class":353}," firstFocusable) {\n",[344,1813,1814,1817,1820],{"class":346,"line":670},[344,1815,1816],{"class":353}," e.",[344,1818,1819],{"class":423},"preventDefault",[344,1821,813],{"class":353},[344,1823,1824,1827,1830],{"class":346,"line":681},[344,1825,1826],{"class":353}," lastFocusable.",[344,1828,1829],{"class":423},"focus",[344,1831,813],{"class":353},[344,1833,1834],{"class":346,"line":690},[344,1835,729],{"class":353},[344,1837,1838,1840,1842],{"class":346,"line":703},[344,1839,706],{"class":353},[344,1841,709],{"class":367},[344,1843,427],{"class":353},[344,1845,1846,1848,1850,1852],{"class":346,"line":714},[344,1847,684],{"class":367},[344,1849,1806],{"class":353},[344,1851,803],{"class":367},[344,1853,1854],{"class":353}," lastFocusable) {\n",[344,1856,1857,1859,1861],{"class":346,"line":726},[344,1858,1816],{"class":353},[344,1860,1819],{"class":423},[344,1862,813],{"class":353},[344,1864,1865,1868,1870],{"class":346,"line":732},[344,1866,1867],{"class":353}," firstFocusable.",[344,1869,1829],{"class":423},[344,1871,813],{"class":353},[344,1873,1874],{"class":346,"line":752},[344,1875,729],{"class":353},[344,1877,1878],{"class":346,"line":758},[344,1879,729],{"class":353},[344,1881,1882],{"class":346,"line":763},[344,1883,819],{"class":353},[344,1885,1886],{"class":346,"line":795},[344,1887,361],{"emptyLinePlaceholder":360},[344,1889,1890,1893,1896,1898,1901],{"class":346,"line":816},[344,1891,1892],{"class":353}," containerRef.current.",[344,1894,1895],{"class":423},"addEventListener",[344,1897,606],{"class":353},[344,1899,1900],{"class":350},"'keydown'",[344,1902,1903],{"class":353},", handleKeyDown);\n",[344,1905,1906,1908,1910],{"class":346,"line":822},[344,1907,1892],{"class":353},[344,1909,1829],{"class":423},[344,1911,813],{"class":353},[344,1913,1914],{"class":346,"line":827},[344,1915,361],{"emptyLinePlaceholder":360},[344,1917,1918,1920,1922,1924],{"class":346,"line":861},[344,1919,735],{"class":367},[344,1921,454],{"class":353},[344,1923,457],{"class":367},[344,1925,427],{"class":353},[344,1927,1928,1931,1934,1936,1938],{"class":346,"line":878},[344,1929,1930],{"class":353}," containerRef.current?.",[344,1932,1933],{"class":423},"removeEventListener",[344,1935,606],{"class":353},[344,1937,1900],{"class":350},[344,1939,1903],{"class":353},[344,1941,1942,1945,1947],{"class":346,"line":883},[344,1943,1944],{"class":353}," previousFocusRef.current?.",[344,1946,1829],{"class":423},[344,1948,813],{"class":353},[344,1950,1951],{"class":346,"line":888},[344,1952,819],{"class":353},[344,1954,1955],{"class":346,"line":918},[344,1956,1957],{"class":353}," }, [isActive, containerRef]);\n",[344,1959,1960],{"class":346,"line":923},[344,1961,500],{"class":353},[248,1963,1965],{"id":1964},"_4-state-announcements-dynamic-content-handling","4. State Announcements & Dynamic Content Handling",[166,1967,1968],{},"Ensure screen readers announce modal state changes and dynamically loaded content without disrupting the user.",[185,1970,1971,1982,1985,1988],{},[188,1972,1973,1974,1977,1978,1981],{},"Use ",[191,1975,1976],{},"aria-live=\"polite\""," or ",[191,1979,1980],{},"aria-live=\"assertive\""," for state changes",[188,1983,1984],{},"Avoid auto-announcing non-critical content",[188,1986,1987],{},"Defer heavy content rendering until after focus is established",[188,1989,1990,1991,1994],{},"Synchronize ",[191,1992,1993],{},"aria-hidden"," on sibling elements to prevent background reading",[166,1996,1997,1999],{},[181,1998,277],{}," Validate with VoiceOver (macOS) and NVDA (Windows) to confirm accurate announcement of \"Dialog opened\" and \"Dialog closed\".",[248,2001,2003],{"id":2002},"_5-automated-manual-testing-workflow","5. Automated & Manual Testing Workflow",[166,2005,2006],{},"Establish a repeatable QA pipeline combining programmatic checks and assistive technology validation.",[185,2008,2009,2016,2019,2022],{},[188,2010,2011,2012,2015],{},"Integrate ",[191,2013,2014],{},"jest-axe"," for CI\u002FCD regression testing",[188,2017,2018],{},"Use Playwright for simulated keyboard and focus trap validation",[188,2020,2021],{},"Conduct manual screen reader testing across OS\u002Fbrowser combinations",[188,2023,2024],{},"Audit scroll-lock implementation for viewport shift prevention",[166,2026,2027,2029],{},[181,2028,277],{}," Document test results in a shared accessibility matrix for product team sign-off.",[248,2031,2033],{"id":2032},"common-implementation-pitfalls","Common Implementation Pitfalls",[185,2035,2036,2039,2045,2052,2055],{},[188,2037,2038],{},"Failing to restore focus to the trigger element after modal closure, causing keyboard navigation reset",[188,2040,2041,2042,2044],{},"Using ",[191,2043,1993],{}," on the entire document instead of scoping it to sibling elements, breaking screen reader context",[188,2046,2047,2048,2051],{},"Relying on CSS ",[191,2049,2050],{},"pointer-events: none"," for backdrop inertness instead of programmatic focus blocking",[188,2053,2054],{},"Hydration mismatches caused by rendering modal state differently on server vs client",[188,2056,2057,2058,1977,2060,2063],{},"Missing ",[191,2059,301],{},[191,2061,2062],{},"aria-describedby",", resulting in unnamed dialog announcements",[248,2065,2067],{"id":2066},"frequently-asked-questions","Frequently Asked Questions",[166,2069,2070,2073,2074,525,2077,2080,2081,525,2084,2086,2087,2090,2091,177],{},[181,2071,2072],{},"Why can't I handle modal focus trapping directly in a Next.js Server Component?","\nServer Components execute on the server and render to static HTML. They lack access to the browser's ",[191,2075,2076],{},"window",[191,2078,2079],{},"document",", or DOM APIs required for ",[191,2082,2083],{},"focus()",[191,2085,1895],{},", and ",[191,2088,2089],{},"requestAnimationFrame",". All interactive accessibility features must be delegated to a ",[191,2092,228],{},[166,2094,2095,2098,2099,2102,2103,2106,2107,2110,2111,2113],{},[181,2096,2097],{},"How do I prevent scroll shifting when a modal opens in Next.js 14?","\nApply a CSS class to the ",[191,2100,2101],{},"\u003Cbody>"," that sets ",[191,2104,2105],{},"overflow: hidden"," and compensates for scrollbar width using ",[191,2108,2109],{},"padding-right",". Ensure this style is applied synchronously with the modal's ",[191,2112,265],{}," state to prevent layout thrashing.",[166,2115,2116,2122,2123,2125,2126,239,2128,2130],{},[181,2117,2118,2119,2121],{},"Is ",[191,2120,242],{}," sufficient for focus trapping?","\nNo. ",[191,2124,242],{}," is a semantic hint for assistive technologies, but it does not programmatically restrict keyboard focus. You must implement a manual focus trap using JavaScript to intercept ",[191,2127,1466],{},[191,2129,1469],{}," key events.",[166,2132,2133,2136,2137,2140,2141,2144],{},[181,2134,2135],{},"How should I handle dynamic form content inside the modal?","\nRender the form structure initially, but defer heavy validation or data fetching until the modal receives focus. Use ",[191,2138,2139],{},"aria-live"," regions to announce validation errors, and ensure all form controls have explicit ",[191,2142,2143],{},"\u003Clabel>"," associations.",[1286,2146,2147],{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":340,"searchDepth":357,"depth":357,"links":2149},[2150,2151,2155,2159,2160,2161,2162],{"id":250,"depth":357,"text":251},{"id":281,"depth":357,"text":282,"children":2152},[2153],{"id":329,"depth":364,"text":2154},"AccessibleModal.tsx (Client Component)",{"id":1438,"depth":357,"text":1439,"children":2156},[2157],{"id":1486,"depth":364,"text":2158},"useFocusTrap.ts (Custom Hook)",{"id":1964,"depth":357,"text":1965},{"id":2002,"depth":357,"text":2003},{"id":2032,"depth":357,"text":2033},{"id":2066,"depth":357,"text":2067},null,"Build accessible modals in Next.js 14 with a clear server-client boundary, robust focus control, and compliant keyboard interactions.","md",{},false,{"title":151,"description":2164},"En0L6L4V7LwYhoN9Zk4AoTkX0Byp5HxyNjytYmNObmQ",[2171,2201,2202],{"title":5,"path":6,"stem":7,"children":2172},[2173,2174,2177,2180,2186,2192,2198],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2175},[2176],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2178},[2179],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2181},[2182,2183],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2184},[2185],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2187},[2188,2189],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2190},[2191],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2193},[2194,2195],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2196},[2197],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2199},[2200],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},{"title":71,"path":72,"stem":73,"children":2203},[2204,2205,2211,2217,2220,2229,2238],{"title":76,"path":72,"stem":77},{"title":79,"path":80,"stem":81,"children":2206},[2207,2208],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":2209},[2210],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":2212},[2213,2214],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":2215},[2216],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2218},[2219],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2221},[2222,2223,2226],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2224},[2225],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2227},[2228],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2230},[2231,2232,2235],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2233},[2234],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2236},[2237],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2239},[2240,2241],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2242},[2243],{"title":151,"path":152,"stem":153},1778094796156]