[{"data":1,"prerenderedAt":3899},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002F":314,"content-navigation":3747},[4,84,88,216],{"title":5,"path":6,"stem":7,"children":8},"Core Accessibility Principles For Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks","core-accessibility-principles-for-modern-frameworks",[9,12,18,24,36,48,66,78],{"title":10,"path":6,"stem":11},"Core Accessibility Principles for Modern Frameworks","core-accessibility-principles-for-modern-frameworks\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Accessible Color Contrast & Theming","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming","core-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002Findex",[17],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Accessible Form Validation & Error States","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states","core-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Focus Management Strategies for SPAs","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Findex",[29,30],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Handling Focus Restoration After Dynamic Route Changes","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Keyboard Navigation Patterns for Modals","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Findex",[41,42],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"Building Accessible Dropdowns Without External UI Kits","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits\u002Findex",[47],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Reduced Motion & Animation Accessibility","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Findex",[53,54,60],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Accessible Loading Skeletons and Spinners","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Respecting prefers-reduced-motion in React and CSS","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css","core-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":70},"Screen Reader Compatibility Testing","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[71,72],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":76},"Testing ARIA Live Regions with Jest and Testing Library","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library\u002Findex",[77],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":82},"Semantic HTML vs ARIA in Component Trees","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees","core-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002Findex",[83],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},"Modern Framework Accessibility","\u002F","index",{"title":89,"path":90,"stem":91,"children":92},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[93,96,108,132,156,162,180,204],{"title":94,"path":90,"stem":95},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":97,"path":98,"stem":99,"children":100},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[101,102],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Building Accessible Tabs in React Without Radix UI","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Accessible Data Tables & Grids in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Findex",[113,114,120,126],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Accessible Pagination for React Data Tables","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002Findex",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Building a Sortable Accessible Data Table in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"Virtualizing Long Lists Accessibly in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react","react-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002Findex",[131],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"Dynamic Content & State Announcements","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[137,138,144,150],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Accessible Toast Notifications in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Faccessible-toast-notifications-in-react\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Announcing Client-Side Route Changes in React","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Fannouncing-client-side-route-changes-in-react\u002Findex",[149],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"React Context for Accessibility Preferences","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences\u002Findex",[155],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":160},"Form Handling with React Hook Form & A11y","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[161],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":166},"Next.js App Router Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[167,168,174],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":172},"Skip Links in Next.js App Router","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router\u002Findex",[173],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":178},"Next.js Dynamic Imports & Keyboard Navigation","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002Findex",[179],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":184},"React Hooks for Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[185,186,192,198],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":190},"Building a useAnnouncer Hook for Live Regions","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions\u002Findex",[191],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":196},"Fixing Focus Trap Issues in React Portals","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002Findex",[197],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":202},"Making React useEffect Accessible for Screen Readers","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers\u002Findex",[203],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":208},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[209,210],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":214},"Accessible Modals in Next.js 14 Server Components","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002Findex",[215],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":220},"Testing And Automating Accessibility","\u002Ftesting-and-automating-accessibility","testing-and-automating-accessibility",[221,224,242,260,278,296],{"title":222,"path":218,"stem":223},"Testing & Automating Accessibility","testing-and-automating-accessibility\u002Findex",{"title":225,"path":226,"stem":227,"children":228},"Accessibility Audits with Lighthouse","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Findex",[229,230,236],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":234},"Interpreting Lighthouse Accessibility Scores","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Finterpreting-lighthouse-accessibility-scores\u002Findex",[235],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":240},"Setting Lighthouse CI Accessibility Budgets","\u002Ftesting-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets","testing-and-automating-accessibility\u002Faccessibility-audits-with-lighthouse\u002Fsetting-lighthouse-ci-accessibility-budgets\u002Findex",[241],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":246},"Automated Accessibility Testing with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Findex",[247,248,254],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":252},"Catching Color Contrast Failures with axe-core","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fcatching-color-contrast-failures-with-axe-core\u002Findex",[253],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":258},"Writing Custom axe-core Rules","\u002Ftesting-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules","testing-and-automating-accessibility\u002Fautomated-accessibility-testing-with-axe-core\u002Fwriting-custom-axe-core-rules\u002Findex",[259],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":264},"Component Testing with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Findex",[265,266,272],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":270},"Debugging jest-axe Violations in CI","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Fdebugging-jest-axe-violations-in-ci\u002Findex",[271],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":276},"Testing React Components with jest-axe","\u002Ftesting-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe","testing-and-automating-accessibility\u002Fcomponent-testing-with-jest-axe\u002Ftesting-react-components-with-jest-axe\u002Findex",[277],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":282},"End-to-End Accessibility Testing with Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Findex",[283,284,290],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":288},"Asserting Focus Order in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fasserting-focus-order-in-playwright\u002Findex",[289],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":294},"Keyboard Navigation Tests in Playwright","\u002Ftesting-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright","testing-and-automating-accessibility\u002Fend-to-end-accessibility-testing-with-playwright\u002Fkeyboard-navigation-tests-in-playwright\u002Findex",[295],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":300},"Gating Accessibility in CI\u002FCD Pipelines","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Findex",[301,302,308],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":306},"Accessibility Regression Testing in GitHub Actions","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Faccessibility-regression-testing-in-github-actions\u002Findex",[307],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":312},"Failing Pull Requests on axe Violations","\u002Ftesting-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations","testing-and-automating-accessibility\u002Fgating-accessibility-in-ci-cd-pipelines\u002Ffailing-pull-requests-on-axe-violations\u002Findex",[313],{"title":309,"path":310,"stem":311},{"id":315,"title":37,"body":316,"date":3740,"description":3741,"extension":3742,"image":3740,"meta":3743,"modifiedAt":3740,"navigation":703,"noindex":3744,"path":38,"publishedAt":3740,"seo":3745,"stem":39,"updatedAt":3740,"__hash__":3746},"content\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Findex.md",{"type":317,"value":318,"toc":3717},"minimark",[319,323,332,338,363,368,382,385,390,393,544,568,570,574,577,582,630,637,649,657,667,1010,1030,1032,1036,1044,1048,1071,1078,1083,1087,1749,1753,1768,1963,1969,1971,1975,1981,1985,2023,2031,2035,2041,2262,2275,2277,2281,2284,2288,2322,2330,2335,2339,2988,2995,3002,3120,3132,3134,3138,3219,3221,3225,3228,3248,3570,3585,3587,3591,3617,3631,3646,3655,3673,3687,3689,3693,3713],[320,321,37],"h1",{"id":322},"keyboard-navigation-patterns-for-modals",[324,325,326,327,331],"p",{},"Implementing robust keyboard navigation for modal dialogs requires precise focus management, state synchronization, and strict adherence to WAI-ARIA specifications. This guide bridges foundational ",[328,329,10],"a",{"href":330},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002F"," with practical, framework-agnostic implementation patterns for trapping focus, handling escape keys, and managing DOM portals without breaking assistive technology context.",[324,333,334],{},[335,336,337],"strong",{},"WCAG Success Criteria Addressed:",[339,340,341,348,353,358],"ul",{},[342,343,344],"li",{},[345,346,347],"code",{},"2.1.1 Keyboard",[342,349,350],{},[345,351,352],{},"2.1.2 No Keyboard Trap",[342,354,355],{},[345,356,357],{},"2.4.3 Focus Order",[342,359,360],{},[345,361,362],{},"4.1.2 Name, Role, Value",[324,364,365],{},[335,366,367],{},"Core Implementation Requirements:",[339,369,370,373,376,379],{},[342,371,372],{},"Modal dialogs must trap focus until explicitly dismissed.",[342,374,375],{},"Escape key behavior must be consistent across all viewport breakpoints.",[342,377,378],{},"Framework state and routing must sync with modal visibility to preserve browser history.",[342,380,381],{},"ARIA attributes must be dynamically applied based on mount\u002Funmount lifecycles.",[383,384],"hr",{},[386,387,389],"h2",{"id":388},"the-modal-keyboard-interaction-model","The Modal Keyboard Interaction Model",[324,391,392],{},"Before writing a single line of trap logic, it helps to internalize the full keyboard contract a modal must satisfy. A modal is not merely a styled overlay; it is a temporary, self-contained navigation context. While it is open, the keyboard must behave as if the rest of the page does not exist. The diagram below maps the complete interaction surface that the following sections implement.",[394,395,402,403,402,407,402,411,402,402,418,402,428,402,436,402,402,442,402,448,402,452,402,402,457,402,464,402,402,469,402,474,402,402,479,402,482,402,402,486,402,489,402,402,493,402,498,402,501,402,506,402,509,402,512,402,515,402,520,402,402,524,402,528,402,402,532,402,538,402,541],"svg",{"role":396,"ariaLabelledBy":397,"viewBox":400,"style":401},"img",[398,399],"modalKbTitle","modalKbDesc","0 0 760 360","width:100%;height:auto;max-width:760px","\n  ",[404,405,406],"title",{"id":398},"Keyboard interaction map for an accessible modal dialog",[408,409,410],"desc",{"id":399},"A trigger button opens a dialog. Inside the dialog, Tab and Shift+Tab cycle focus between the first and last focusable elements without leaving the dialog, satisfying WCAG 2.1.1 and 2.4.3. Escape closes the dialog without creating a keyboard trap, satisfying WCAG 2.1.2, and focus returns to the original trigger.",[412,413],"rect",{"x":414,"y":414,"width":415,"height":416,"fill":417},"0","760","360","var(--surface)",[412,419],{"style":420,"x":421,"y":422,"width":423,"height":424,"rx":425,"fill":426,"stroke":427},"stroke-width:2","24","150","140","56","6","var(--primary-soft)","currentColor",[429,430,435],"text",{"style":431,"x":432,"y":433,"fill":434},"font-size:14px;text-anchor:middle","94","174","var(--text)","Trigger",[429,437,441],{"style":438,"x":432,"y":439,"fill":440},"font-size:12px;text-anchor:middle","194","var(--muted)","button",[443,444],"line",{"style":420,"x1":445,"y1":446,"x2":447,"y2":446,"stroke":427},"164","178","244",[449,450],"polygon",{"points":451,"fill":427},"244,178 232,172 232,184",[429,453,456],{"style":438,"x":454,"y":455,"fill":440},"204","168","open",[412,458],{"style":420,"x":459,"y":460,"width":416,"height":461,"rx":462,"fill":417,"stroke":463},"248","40","280","8","var(--primary-strong)",[429,465,468],{"style":431,"x":466,"y":467,"fill":463},"428","66","role=\"dialog\" aria-modal=\"true\"",[412,470],{"style":420,"x":471,"y":472,"width":461,"height":473,"rx":425,"fill":426,"stroke":427},"288","92","48",[429,475,478],{"style":476,"x":466,"y":477,"fill":434},"font-size:13px;text-anchor:middle","121","First focusable element",[412,480],{"style":420,"x":471,"y":481,"width":461,"height":473,"rx":425,"fill":417,"stroke":427},"156",[429,483,485],{"style":476,"x":466,"y":484,"fill":434},"185","Interactive content",[412,487],{"style":420,"x":471,"y":488,"width":461,"height":473,"rx":425,"fill":426,"stroke":427},"220",[429,490,492],{"style":476,"x":466,"y":491,"fill":434},"249","Last focusable (Close)",[443,494],{"style":420,"x1":495,"y1":496,"x2":495,"y2":447,"stroke":497},"600","116","var(--primary)",[449,499],{"points":500,"fill":497},"600,244 594,232 606,232",[429,502,505],{"style":438,"x":503,"y":504,"fill":497},"640","160","Tab",[429,507,508],{"style":438,"x":503,"y":446,"fill":497},"2.4.3",[443,510],{"style":420,"x1":511,"y1":447,"x2":511,"y2":496,"stroke":497},"256",[449,513],{"points":514,"fill":497},"256,116 250,128 262,128",[429,516,519],{"style":438,"x":517,"y":518,"fill":497},"216","180","Shift",[429,521,523],{"style":438,"x":517,"y":522,"fill":497},"198","+Tab",[443,525],{"style":420,"x1":466,"y1":526,"x2":466,"y2":527,"stroke":427},"320","345",[429,529,531],{"style":438,"x":466,"y":530,"fill":440},"338","Escape closes (2.1.2 No Keyboard Trap)",[533,534],"path",{"style":535,"d":536,"fill":537,"stroke":463},"stroke-width:2;stroke-dasharray:6 4","M 288 296 Q 120 330 94 210","none",[449,539],{"points":540,"fill":463},"94,210 88,222 100,222",[429,542,543],{"style":438,"x":422,"y":526,"fill":463},"focus returns on close",[324,545,546,547,549,550,553,554,557,558,560,561,564,565,567],{},"Three guarantees define a compliant modal. First, ",[345,548,505],{}," and ",[345,551,552],{},"Shift+Tab"," cycle within the dialog and never reach background content (",[345,555,556],{},"2.1.1",", ",[345,559,508],{},"). Second, ",[345,562,563],{},"Escape"," always offers an exit so the user is never stranded (",[345,566,352],{},"). Third, focus returns to the element that opened the dialog, preserving the user's place in the document. Every code sample below exists to enforce one of these three guarantees.",[383,569],{},[386,571,573],{"id":572},"structural-markup-aria-role-assignment","Structural Markup & ARIA Role Assignment",[324,575,576],{},"Establishing a semantically correct foundation is non-negotiable. Assistive technologies rely on explicit role declarations and labeled relationships to announce dialog intent before any interactive behaviors are applied.",[324,578,579],{},[335,580,581],{},"Implementation Guidelines:",[339,583,584,595,606,613,623],{},[342,585,586,587,590,591,594],{},"Prefer the native ",[345,588,589],{},"\u003Cdialog>"," element where browser support permits. It provides built-in focus management and the ",[345,592,593],{},"showModal()"," API, which automatically restricts tab focus to the dialog.",[342,596,597,598,601,602,605],{},"For heavily customized implementations, fall back to ",[345,599,600],{},"role=\"dialog\""," paired with ",[345,603,604],{},"aria-modal=\"true\"",".",[342,607,608,609,612],{},"Ensure ",[345,610,611],{},"aria-labelledby"," explicitly references a visible heading ID within the modal container.",[342,614,615,616,618,619,622],{},"Distinguish between ",[345,617,600],{}," (standard interaction) and ",[345,620,621],{},"role=\"alertdialog\""," (requires explicit acknowledgment before dismissal).",[342,624,625,626,629],{},"Apply the ",[345,627,628],{},"inert"," attribute to background content to prevent accidental focus drift and screen reader traversal.",[324,631,632,633,636],{},"When evaluating markup strategies, reference our analysis of ",[328,634,79],{"href":635},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002F"," to avoid redundant or conflicting attribute declarations that degrade screen reader output.",[324,638,639,642,643,549,646,648],{},[335,640,641],{},"Testing Hook:"," Verify that screen readers announce the modal role and title immediately upon open. Cross-check against automated linters to ensure ",[345,644,645],{},"aria-modal",[345,647,628],{}," are applied synchronously with DOM insertion.",[650,651,653,654,656],"h3",{"id":652},"native-dialog-as-a-baseline","Native ",[345,655,589],{}," as a Baseline",[324,658,659,660,549,663,666],{},"When you can rely on the native element, you inherit a large amount of correct behavior for free. The browser supplies the focus trap, the top-layer rendering (immune to ",[345,661,662],{},"z-index",[345,664,665],{},"overflow"," clipping), and the backdrop pseudo-element. The accessibility object model already exposes the dialog role.",[668,669,674],"pre",{"className":670,"code":671,"language":672,"meta":673,"style":673},"language-tsx shiki shiki-themes github-light github-dark","import { useEffect, useRef } from 'react';\n\nexport function NativeDialog({ isOpen, onClose, titleId, children }) {\n  const ref = useRef\u003CHTMLDialogElement>(null);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    if (isOpen && !el.open) el.showModal();\n    if (!isOpen && el.open) el.close();\n  }, [isOpen]);\n\n  return (\n    \u003Cdialog\n      ref={ref}\n      aria-labelledby={titleId}\n      onClose={onClose}            \u002F\u002F fires on Escape and el.close()\n      onCancel={(e) => { e.preventDefault(); onClose(); }}\n    >\n      {children}\n    \u003C\u002Fdialog>\n  );\n}\n","tsx","",[345,675,676,698,705,743,774,779,794,808,828,851,873,879,884,893,903,915,926,941,974,980,986,998,1004],{"__ignoreMap":673},[677,678,680,684,688,691,695],"span",{"class":443,"line":679},1,[677,681,683],{"class":682},"szBVR","import",[677,685,687],{"class":686},"sVt8B"," { useEffect, useRef } ",[677,689,690],{"class":682},"from",[677,692,694],{"class":693},"sZZnC"," 'react'",[677,696,697],{"class":686},";\n",[677,699,701],{"class":443,"line":700},2,[677,702,704],{"emptyLinePlaceholder":703},true,"\n",[677,706,708,711,714,718,721,725,727,730,732,735,737,740],{"class":443,"line":707},3,[677,709,710],{"class":682},"export",[677,712,713],{"class":682}," function",[677,715,717],{"class":716},"sScJk"," NativeDialog",[677,719,720],{"class":686},"({ ",[677,722,724],{"class":723},"s4XuR","isOpen",[677,726,557],{"class":686},[677,728,729],{"class":723},"onClose",[677,731,557],{"class":686},[677,733,734],{"class":723},"titleId",[677,736,557],{"class":686},[677,738,739],{"class":723},"children",[677,741,742],{"class":686}," }) {\n",[677,744,746,749,753,756,759,762,765,768,771],{"class":443,"line":745},4,[677,747,748],{"class":682},"  const",[677,750,752],{"class":751},"sj4cs"," ref",[677,754,755],{"class":682}," =",[677,757,758],{"class":716}," useRef",[677,760,761],{"class":686},"\u003C",[677,763,764],{"class":716},"HTMLDialogElement",[677,766,767],{"class":686},">(",[677,769,770],{"class":751},"null",[677,772,773],{"class":686},");\n",[677,775,777],{"class":443,"line":776},5,[677,778,704],{"emptyLinePlaceholder":703},[677,780,782,785,788,791],{"class":443,"line":781},6,[677,783,784],{"class":716},"  useEffect",[677,786,787],{"class":686},"(() ",[677,789,790],{"class":682},"=>",[677,792,793],{"class":686}," {\n",[677,795,797,800,803,805],{"class":443,"line":796},7,[677,798,799],{"class":682},"    const",[677,801,802],{"class":751}," el",[677,804,755],{"class":682},[677,806,807],{"class":686}," ref.current;\n",[677,809,811,814,817,820,823,826],{"class":443,"line":810},8,[677,812,813],{"class":682},"    if",[677,815,816],{"class":686}," (",[677,818,819],{"class":682},"!",[677,821,822],{"class":686},"el) ",[677,824,825],{"class":682},"return",[677,827,697],{"class":686},[677,829,831,833,836,839,842,845,848],{"class":443,"line":830},9,[677,832,813],{"class":682},[677,834,835],{"class":686}," (isOpen ",[677,837,838],{"class":682},"&&",[677,840,841],{"class":682}," !",[677,843,844],{"class":686},"el.open) el.",[677,846,847],{"class":716},"showModal",[677,849,850],{"class":686},"();\n",[677,852,854,856,858,860,863,865,868,871],{"class":443,"line":853},10,[677,855,813],{"class":682},[677,857,816],{"class":686},[677,859,819],{"class":682},[677,861,862],{"class":686},"isOpen ",[677,864,838],{"class":682},[677,866,867],{"class":686}," el.open) el.",[677,869,870],{"class":716},"close",[677,872,850],{"class":686},[677,874,876],{"class":443,"line":875},11,[677,877,878],{"class":686},"  }, [isOpen]);\n",[677,880,882],{"class":443,"line":881},12,[677,883,704],{"emptyLinePlaceholder":703},[677,885,887,890],{"class":443,"line":886},13,[677,888,889],{"class":682},"  return",[677,891,892],{"class":686}," (\n",[677,894,896,899],{"class":443,"line":895},14,[677,897,898],{"class":686},"    \u003C",[677,900,902],{"class":901},"s9eBZ","dialog\n",[677,904,906,909,912],{"class":443,"line":905},15,[677,907,908],{"class":716},"      ref",[677,910,911],{"class":682},"=",[677,913,914],{"class":686},"{ref}\n",[677,916,918,921,923],{"class":443,"line":917},16,[677,919,920],{"class":716},"      aria-labelledby",[677,922,911],{"class":682},[677,924,925],{"class":686},"{titleId}\n",[677,927,929,932,934,937],{"class":443,"line":928},17,[677,930,931],{"class":716},"      onClose",[677,933,911],{"class":682},[677,935,936],{"class":686},"{onClose}            ",[677,938,940],{"class":939},"sJ8bj","\u002F\u002F fires on Escape and el.close()\n",[677,942,944,947,949,952,955,958,960,963,966,969,971],{"class":443,"line":943},18,[677,945,946],{"class":716},"      onCancel",[677,948,911],{"class":682},[677,950,951],{"class":686},"{(",[677,953,954],{"class":723},"e",[677,956,957],{"class":686},") ",[677,959,790],{"class":682},[677,961,962],{"class":686}," { e.",[677,964,965],{"class":716},"preventDefault",[677,967,968],{"class":686},"(); ",[677,970,729],{"class":716},[677,972,973],{"class":686},"(); }}\n",[677,975,977],{"class":443,"line":976},19,[677,978,979],{"class":686},"    >\n",[677,981,983],{"class":443,"line":982},20,[677,984,985],{"class":686},"      {children}\n",[677,987,989,992,995],{"class":443,"line":988},21,[677,990,991],{"class":686},"    \u003C\u002F",[677,993,994],{"class":901},"dialog",[677,996,997],{"class":686},">\n",[677,999,1001],{"class":443,"line":1000},22,[677,1002,1003],{"class":686},"  );\n",[677,1005,1007],{"class":443,"line":1006},23,[677,1008,1009],{"class":686},"}\n",[324,1011,1012,1013,1015,1016,1018,1019,1022,1023,1025,1026,1029],{},"The ",[345,1014,593],{}," call is what activates native focus trapping and the inert-by-default backdrop; a plain ",[345,1017,456],{}," attribute does not. The ",[345,1020,1021],{},"onCancel"," handler lets you intercept the native ",[345,1024,563],{}," behavior when you need a confirmation step before dismissal, which becomes important for ",[345,1027,1028],{},"alertdialog"," flows discussed below.",[383,1031],{},[386,1033,1035],{"id":1034},"focus-trapping-tab-order-management","Focus Trapping & Tab Order Management",[324,1037,1038,1039,549,1041,1043],{},"A modal must create a strict cyclic focus loop that prevents users from tabbing outside the container while maintaining logical navigation order. This requires intercepting ",[345,1040,505],{},[345,1042,552],{}," events at the container level and dynamically calculating focusable boundaries.",[324,1045,1046],{},[335,1047,581],{},[339,1049,1050,1053,1065,1068],{},[342,1051,1052],{},"Identify the first and last focusable elements to establish boundary conditions.",[342,1054,1055,1056,1059,1060,86,1062,1064],{},"Intercept ",[345,1057,1058],{},"keydown"," events for ",[345,1061,505],{},[345,1063,552],{}," and programmatically redirect focus when boundaries are breached.",[342,1066,1067],{},"Account for dynamically rendered inputs, buttons, or framework portals that may alter the DOM tree post-mount.",[342,1069,1070],{},"Always restore focus to the original trigger element on close to preserve spatial context and prevent disorientation.",[324,1072,1073,1074,1077],{},"Framework reactivity can easily break focus traps if DOM updates occur asynchronously. Align your implementation with established ",[328,1075,25],{"href":1076},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002F"," to ensure re-renders do not detach the active element from the trap logic.",[324,1079,1080,1082],{},[335,1081,641],{}," Execute keyboard-only navigation across nested components and dynamically injected form fields. Validate that focus remains strictly within the modal and correctly returns to the trigger upon dismissal.",[650,1084,1086],{"id":1085},"react-implementation-custom-focus-trap-hook","React Implementation: Custom Focus Trap Hook",[668,1088,1090],{"className":670,"code":1089,"language":672,"meta":673,"style":673},"import { useEffect, useRef, useCallback } from 'react';\n\nconst FOCUSABLE_SELECTORS = [\n  'a[href]', 'button:not([disabled])', 'input:not([disabled])',\n  'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex=\"-1\"])'\n].join(', ');\n\nexport function useFocusTrap(isOpen: boolean) {\n  const containerRef = useRef\u003CHTMLDivElement>(null);\n  const triggerRef = useRef\u003CHTMLElement | null>(null);\n\n  const handleKeyDown = useCallback((e: KeyboardEvent) => {\n    if (!isOpen || !containerRef.current) return;\n    if (e.key !== 'Tab') return;\n\n    const focusableElements = Array.from(\n      containerRef.current.querySelectorAll\u003CHTMLElement>(FOCUSABLE_SELECTORS)\n    );\n    if (focusableElements.length === 0) return;\n\n    const firstEl = focusableElements[0];\n    const lastEl = focusableElements[focusableElements.length - 1];\n\n    if (e.shiftKey && document.activeElement === firstEl) {\n      e.preventDefault();\n      lastEl.focus();\n    } else if (!e.shiftKey && document.activeElement === lastEl) {\n      e.preventDefault();\n      firstEl.focus();\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen) {\n      triggerRef.current = document.activeElement as HTMLElement;\n      document.addEventListener('keydown', handleKeyDown);\n      \u002F\u002F Defer focus to first element to avoid hydration\u002Flayout shift conflicts\n      requestAnimationFrame(() => {\n        const first = containerRef.current?.querySelector\u003CHTMLElement>(FOCUSABLE_SELECTORS);\n        first?.focus();\n      });\n    }\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n      if (isOpen && triggerRef.current) {\n        triggerRef.current.focus();\n      }\n    };\n  }, [isOpen, handleKeyDown]);\n\n  return containerRef;\n}\n",[345,1091,1092,1105,1109,1122,1140,1155,1171,1175,1197,1219,1247,1251,1279,1301,1320,1324,1341,1361,1366,1388,1392,1409,1431,1435,1454,1464,1475,1503,1512,1522,1528,1533,1538,1549,1557,1575,1592,1598,1610,1637,1647,1653,1658,1663,1676,1690,1703,1713,1719,1725,1731,1736,1744],{"__ignoreMap":673},[677,1093,1094,1096,1099,1101,1103],{"class":443,"line":679},[677,1095,683],{"class":682},[677,1097,1098],{"class":686}," { useEffect, useRef, useCallback } ",[677,1100,690],{"class":682},[677,1102,694],{"class":693},[677,1104,697],{"class":686},[677,1106,1107],{"class":443,"line":700},[677,1108,704],{"emptyLinePlaceholder":703},[677,1110,1111,1114,1117,1119],{"class":443,"line":707},[677,1112,1113],{"class":682},"const",[677,1115,1116],{"class":751}," FOCUSABLE_SELECTORS",[677,1118,755],{"class":682},[677,1120,1121],{"class":686}," [\n",[677,1123,1124,1127,1129,1132,1134,1137],{"class":443,"line":745},[677,1125,1126],{"class":693},"  'a[href]'",[677,1128,557],{"class":686},[677,1130,1131],{"class":693},"'button:not([disabled])'",[677,1133,557],{"class":686},[677,1135,1136],{"class":693},"'input:not([disabled])'",[677,1138,1139],{"class":686},",\n",[677,1141,1142,1145,1147,1150,1152],{"class":443,"line":776},[677,1143,1144],{"class":693},"  'select:not([disabled])'",[677,1146,557],{"class":686},[677,1148,1149],{"class":693},"'textarea:not([disabled])'",[677,1151,557],{"class":686},[677,1153,1154],{"class":693},"'[tabindex]:not([tabindex=\"-1\"])'\n",[677,1156,1157,1160,1163,1166,1169],{"class":443,"line":781},[677,1158,1159],{"class":686},"].",[677,1161,1162],{"class":716},"join",[677,1164,1165],{"class":686},"(",[677,1167,1168],{"class":693},"', '",[677,1170,773],{"class":686},[677,1172,1173],{"class":443,"line":796},[677,1174,704],{"emptyLinePlaceholder":703},[677,1176,1177,1179,1181,1184,1186,1188,1191,1194],{"class":443,"line":810},[677,1178,710],{"class":682},[677,1180,713],{"class":682},[677,1182,1183],{"class":716}," useFocusTrap",[677,1185,1165],{"class":686},[677,1187,724],{"class":723},[677,1189,1190],{"class":682},":",[677,1192,1193],{"class":751}," boolean",[677,1195,1196],{"class":686},") {\n",[677,1198,1199,1201,1204,1206,1208,1210,1213,1215,1217],{"class":443,"line":830},[677,1200,748],{"class":682},[677,1202,1203],{"class":751}," containerRef",[677,1205,755],{"class":682},[677,1207,758],{"class":716},[677,1209,761],{"class":686},[677,1211,1212],{"class":716},"HTMLDivElement",[677,1214,767],{"class":686},[677,1216,770],{"class":751},[677,1218,773],{"class":686},[677,1220,1221,1223,1226,1228,1230,1232,1235,1238,1241,1243,1245],{"class":443,"line":853},[677,1222,748],{"class":682},[677,1224,1225],{"class":751}," triggerRef",[677,1227,755],{"class":682},[677,1229,758],{"class":716},[677,1231,761],{"class":686},[677,1233,1234],{"class":716},"HTMLElement",[677,1236,1237],{"class":682}," |",[677,1239,1240],{"class":751}," null",[677,1242,767],{"class":686},[677,1244,770],{"class":751},[677,1246,773],{"class":686},[677,1248,1249],{"class":443,"line":875},[677,1250,704],{"emptyLinePlaceholder":703},[677,1252,1253,1255,1258,1260,1263,1266,1268,1270,1273,1275,1277],{"class":443,"line":881},[677,1254,748],{"class":682},[677,1256,1257],{"class":751}," handleKeyDown",[677,1259,755],{"class":682},[677,1261,1262],{"class":716}," useCallback",[677,1264,1265],{"class":686},"((",[677,1267,954],{"class":723},[677,1269,1190],{"class":682},[677,1271,1272],{"class":716}," KeyboardEvent",[677,1274,957],{"class":686},[677,1276,790],{"class":682},[677,1278,793],{"class":686},[677,1280,1281,1283,1285,1287,1289,1292,1294,1297,1299],{"class":443,"line":886},[677,1282,813],{"class":682},[677,1284,816],{"class":686},[677,1286,819],{"class":682},[677,1288,862],{"class":686},[677,1290,1291],{"class":682},"||",[677,1293,841],{"class":682},[677,1295,1296],{"class":686},"containerRef.current) ",[677,1298,825],{"class":682},[677,1300,697],{"class":686},[677,1302,1303,1305,1308,1311,1314,1316,1318],{"class":443,"line":895},[677,1304,813],{"class":682},[677,1306,1307],{"class":686}," (e.key ",[677,1309,1310],{"class":682},"!==",[677,1312,1313],{"class":693}," 'Tab'",[677,1315,957],{"class":686},[677,1317,825],{"class":682},[677,1319,697],{"class":686},[677,1321,1322],{"class":443,"line":905},[677,1323,704],{"emptyLinePlaceholder":703},[677,1325,1326,1328,1331,1333,1336,1338],{"class":443,"line":917},[677,1327,799],{"class":682},[677,1329,1330],{"class":751}," focusableElements",[677,1332,755],{"class":682},[677,1334,1335],{"class":686}," Array.",[677,1337,690],{"class":716},[677,1339,1340],{"class":686},"(\n",[677,1342,1343,1346,1349,1351,1353,1355,1358],{"class":443,"line":928},[677,1344,1345],{"class":686},"      containerRef.current.",[677,1347,1348],{"class":716},"querySelectorAll",[677,1350,761],{"class":686},[677,1352,1234],{"class":716},[677,1354,767],{"class":686},[677,1356,1357],{"class":751},"FOCUSABLE_SELECTORS",[677,1359,1360],{"class":686},")\n",[677,1362,1363],{"class":443,"line":943},[677,1364,1365],{"class":686},"    );\n",[677,1367,1368,1370,1373,1376,1379,1382,1384,1386],{"class":443,"line":976},[677,1369,813],{"class":682},[677,1371,1372],{"class":686}," (focusableElements.",[677,1374,1375],{"class":751},"length",[677,1377,1378],{"class":682}," ===",[677,1380,1381],{"class":751}," 0",[677,1383,957],{"class":686},[677,1385,825],{"class":682},[677,1387,697],{"class":686},[677,1389,1390],{"class":443,"line":982},[677,1391,704],{"emptyLinePlaceholder":703},[677,1393,1394,1396,1399,1401,1404,1406],{"class":443,"line":988},[677,1395,799],{"class":682},[677,1397,1398],{"class":751}," firstEl",[677,1400,755],{"class":682},[677,1402,1403],{"class":686}," focusableElements[",[677,1405,414],{"class":751},[677,1407,1408],{"class":686},"];\n",[677,1410,1411,1413,1416,1418,1421,1423,1426,1429],{"class":443,"line":1000},[677,1412,799],{"class":682},[677,1414,1415],{"class":751}," lastEl",[677,1417,755],{"class":682},[677,1419,1420],{"class":686}," focusableElements[focusableElements.",[677,1422,1375],{"class":751},[677,1424,1425],{"class":682}," -",[677,1427,1428],{"class":751}," 1",[677,1430,1408],{"class":686},[677,1432,1433],{"class":443,"line":1006},[677,1434,704],{"emptyLinePlaceholder":703},[677,1436,1438,1440,1443,1445,1448,1451],{"class":443,"line":1437},24,[677,1439,813],{"class":682},[677,1441,1442],{"class":686}," (e.shiftKey ",[677,1444,838],{"class":682},[677,1446,1447],{"class":686}," document.activeElement ",[677,1449,1450],{"class":682},"===",[677,1452,1453],{"class":686}," firstEl) {\n",[677,1455,1457,1460,1462],{"class":443,"line":1456},25,[677,1458,1459],{"class":686},"      e.",[677,1461,965],{"class":716},[677,1463,850],{"class":686},[677,1465,1467,1470,1473],{"class":443,"line":1466},26,[677,1468,1469],{"class":686},"      lastEl.",[677,1471,1472],{"class":716},"focus",[677,1474,850],{"class":686},[677,1476,1478,1481,1484,1487,1489,1491,1494,1496,1498,1500],{"class":443,"line":1477},27,[677,1479,1480],{"class":686},"    } ",[677,1482,1483],{"class":682},"else",[677,1485,1486],{"class":682}," if",[677,1488,816],{"class":686},[677,1490,819],{"class":682},[677,1492,1493],{"class":686},"e.shiftKey ",[677,1495,838],{"class":682},[677,1497,1447],{"class":686},[677,1499,1450],{"class":682},[677,1501,1502],{"class":686}," lastEl) {\n",[677,1504,1506,1508,1510],{"class":443,"line":1505},28,[677,1507,1459],{"class":686},[677,1509,965],{"class":716},[677,1511,850],{"class":686},[677,1513,1515,1518,1520],{"class":443,"line":1514},29,[677,1516,1517],{"class":686},"      firstEl.",[677,1519,1472],{"class":716},[677,1521,850],{"class":686},[677,1523,1525],{"class":443,"line":1524},30,[677,1526,1527],{"class":686},"    }\n",[677,1529,1531],{"class":443,"line":1530},31,[677,1532,878],{"class":686},[677,1534,1536],{"class":443,"line":1535},32,[677,1537,704],{"emptyLinePlaceholder":703},[677,1539,1541,1543,1545,1547],{"class":443,"line":1540},33,[677,1542,784],{"class":716},[677,1544,787],{"class":686},[677,1546,790],{"class":682},[677,1548,793],{"class":686},[677,1550,1552,1554],{"class":443,"line":1551},34,[677,1553,813],{"class":682},[677,1555,1556],{"class":686}," (isOpen) {\n",[677,1558,1560,1563,1565,1567,1570,1573],{"class":443,"line":1559},35,[677,1561,1562],{"class":686},"      triggerRef.current ",[677,1564,911],{"class":682},[677,1566,1447],{"class":686},[677,1568,1569],{"class":682},"as",[677,1571,1572],{"class":716}," HTMLElement",[677,1574,697],{"class":686},[677,1576,1578,1581,1584,1586,1589],{"class":443,"line":1577},36,[677,1579,1580],{"class":686},"      document.",[677,1582,1583],{"class":716},"addEventListener",[677,1585,1165],{"class":686},[677,1587,1588],{"class":693},"'keydown'",[677,1590,1591],{"class":686},", handleKeyDown);\n",[677,1593,1595],{"class":443,"line":1594},37,[677,1596,1597],{"class":939},"      \u002F\u002F Defer focus to first element to avoid hydration\u002Flayout shift conflicts\n",[677,1599,1601,1604,1606,1608],{"class":443,"line":1600},38,[677,1602,1603],{"class":716},"      requestAnimationFrame",[677,1605,787],{"class":686},[677,1607,790],{"class":682},[677,1609,793],{"class":686},[677,1611,1613,1616,1619,1621,1624,1627,1629,1631,1633,1635],{"class":443,"line":1612},39,[677,1614,1615],{"class":682},"        const",[677,1617,1618],{"class":751}," first",[677,1620,755],{"class":682},[677,1622,1623],{"class":686}," containerRef.current?.",[677,1625,1626],{"class":716},"querySelector",[677,1628,761],{"class":686},[677,1630,1234],{"class":716},[677,1632,767],{"class":686},[677,1634,1357],{"class":751},[677,1636,773],{"class":686},[677,1638,1640,1643,1645],{"class":443,"line":1639},40,[677,1641,1642],{"class":686},"        first?.",[677,1644,1472],{"class":716},[677,1646,850],{"class":686},[677,1648,1650],{"class":443,"line":1649},41,[677,1651,1652],{"class":686},"      });\n",[677,1654,1656],{"class":443,"line":1655},42,[677,1657,1527],{"class":686},[677,1659,1661],{"class":443,"line":1660},43,[677,1662,704],{"emptyLinePlaceholder":703},[677,1664,1666,1669,1672,1674],{"class":443,"line":1665},44,[677,1667,1668],{"class":682},"    return",[677,1670,1671],{"class":686}," () ",[677,1673,790],{"class":682},[677,1675,793],{"class":686},[677,1677,1679,1681,1684,1686,1688],{"class":443,"line":1678},45,[677,1680,1580],{"class":686},[677,1682,1683],{"class":716},"removeEventListener",[677,1685,1165],{"class":686},[677,1687,1588],{"class":693},[677,1689,1591],{"class":686},[677,1691,1693,1696,1698,1700],{"class":443,"line":1692},46,[677,1694,1695],{"class":682},"      if",[677,1697,835],{"class":686},[677,1699,838],{"class":682},[677,1701,1702],{"class":686}," triggerRef.current) {\n",[677,1704,1706,1709,1711],{"class":443,"line":1705},47,[677,1707,1708],{"class":686},"        triggerRef.current.",[677,1710,1472],{"class":716},[677,1712,850],{"class":686},[677,1714,1716],{"class":443,"line":1715},48,[677,1717,1718],{"class":686},"      }\n",[677,1720,1722],{"class":443,"line":1721},49,[677,1723,1724],{"class":686},"    };\n",[677,1726,1728],{"class":443,"line":1727},50,[677,1729,1730],{"class":686},"  }, [isOpen, handleKeyDown]);\n",[677,1732,1734],{"class":443,"line":1733},51,[677,1735,704],{"emptyLinePlaceholder":703},[677,1737,1739,1741],{"class":443,"line":1738},52,[677,1740,889],{"class":682},[677,1742,1743],{"class":686}," containerRef;\n",[677,1745,1747],{"class":443,"line":1746},53,[677,1748,1009],{"class":686},[650,1750,1752],{"id":1751},"computing-focusable-elements-reliably","Computing Focusable Elements Reliably",[324,1754,1755,1756,1759,1760,1763,1764,1767],{},"The selector-based approach above is fast but naive: it counts elements that are present in the DOM yet not actually focusable, such as a button inside a ",[345,1757,1758],{},"display: none"," subtree, a ",[345,1761,1762],{},"visibility: hidden"," field, or an element behind a closed ",[345,1765,1766],{},"\u003Cdetails>",". A node that matches the selector but cannot receive focus will silently break the trap, because the \"last\" element you redirect to may not be reachable.",[668,1769,1773],{"className":1770,"code":1771,"language":1772,"meta":673,"style":673},"language-ts shiki shiki-themes github-light github-dark","function isVisible(el: HTMLElement): boolean {\n  \u002F\u002F offsetParent is null for display:none; also catch visibility\u002Fopacity\n  if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') {\n    return false;\n  }\n  const style = getComputedStyle(el);\n  return style.visibility !== 'hidden' && style.display !== 'none';\n}\n\nexport function getFocusable(container: HTMLElement): HTMLElement[] {\n  return Array.from(\n    container.querySelectorAll\u003CHTMLElement>(FOCUSABLE_SELECTORS)\n  ).filter(isVisible);\n}\n","ts",[345,1774,1775,1801,1806,1834,1843,1848,1862,1886,1890,1894,1921,1931,1948,1959],{"__ignoreMap":673},[677,1776,1777,1780,1783,1785,1788,1790,1792,1795,1797,1799],{"class":443,"line":679},[677,1778,1779],{"class":682},"function",[677,1781,1782],{"class":716}," isVisible",[677,1784,1165],{"class":686},[677,1786,1787],{"class":723},"el",[677,1789,1190],{"class":682},[677,1791,1572],{"class":716},[677,1793,1794],{"class":686},")",[677,1796,1190],{"class":682},[677,1798,1193],{"class":751},[677,1800,793],{"class":686},[677,1802,1803],{"class":443,"line":700},[677,1804,1805],{"class":939},"  \u002F\u002F offsetParent is null for display:none; also catch visibility\u002Fopacity\n",[677,1807,1808,1811,1814,1816,1818,1821,1824,1827,1829,1832],{"class":443,"line":707},[677,1809,1810],{"class":682},"  if",[677,1812,1813],{"class":686}," (el.offsetParent ",[677,1815,1450],{"class":682},[677,1817,1240],{"class":751},[677,1819,1820],{"class":682}," &&",[677,1822,1823],{"class":716}," getComputedStyle",[677,1825,1826],{"class":686},"(el).position ",[677,1828,1310],{"class":682},[677,1830,1831],{"class":693}," 'fixed'",[677,1833,1196],{"class":686},[677,1835,1836,1838,1841],{"class":443,"line":745},[677,1837,1668],{"class":682},[677,1839,1840],{"class":751}," false",[677,1842,697],{"class":686},[677,1844,1845],{"class":443,"line":776},[677,1846,1847],{"class":686},"  }\n",[677,1849,1850,1852,1855,1857,1859],{"class":443,"line":781},[677,1851,748],{"class":682},[677,1853,1854],{"class":751}," style",[677,1856,755],{"class":682},[677,1858,1823],{"class":716},[677,1860,1861],{"class":686},"(el);\n",[677,1863,1864,1866,1869,1871,1874,1876,1879,1881,1884],{"class":443,"line":796},[677,1865,889],{"class":682},[677,1867,1868],{"class":686}," style.visibility ",[677,1870,1310],{"class":682},[677,1872,1873],{"class":693}," 'hidden'",[677,1875,1820],{"class":682},[677,1877,1878],{"class":686}," style.display ",[677,1880,1310],{"class":682},[677,1882,1883],{"class":693}," 'none'",[677,1885,697],{"class":686},[677,1887,1888],{"class":443,"line":810},[677,1889,1009],{"class":686},[677,1891,1892],{"class":443,"line":830},[677,1893,704],{"emptyLinePlaceholder":703},[677,1895,1896,1898,1900,1903,1905,1908,1910,1912,1914,1916,1918],{"class":443,"line":853},[677,1897,710],{"class":682},[677,1899,713],{"class":682},[677,1901,1902],{"class":716}," getFocusable",[677,1904,1165],{"class":686},[677,1906,1907],{"class":723},"container",[677,1909,1190],{"class":682},[677,1911,1572],{"class":716},[677,1913,1794],{"class":686},[677,1915,1190],{"class":682},[677,1917,1572],{"class":716},[677,1919,1920],{"class":686},"[] {\n",[677,1922,1923,1925,1927,1929],{"class":443,"line":875},[677,1924,889],{"class":682},[677,1926,1335],{"class":686},[677,1928,690],{"class":716},[677,1930,1340],{"class":686},[677,1932,1933,1936,1938,1940,1942,1944,1946],{"class":443,"line":881},[677,1934,1935],{"class":686},"    container.",[677,1937,1348],{"class":716},[677,1939,761],{"class":686},[677,1941,1234],{"class":716},[677,1943,767],{"class":686},[677,1945,1357],{"class":751},[677,1947,1360],{"class":686},[677,1949,1950,1953,1956],{"class":443,"line":886},[677,1951,1952],{"class":686},"  ).",[677,1954,1955],{"class":716},"filter",[677,1957,1958],{"class":686},"(isVisible);\n",[677,1960,1961],{"class":443,"line":895},[677,1962,1009],{"class":686},[324,1964,1965,1966,1968],{},"Recompute this list on every ",[345,1967,505],{}," keypress rather than caching it at open time. In reactive frameworks the focusable set changes constantly: a validation error reveals a new link, an accordion expands, a loading spinner replaces a submit button. A trap that captures boundaries once at mount will trap users against stale elements. Querying on each keystroke costs microseconds and is the single most reliable defense against framework-driven DOM churn.",[383,1970],{},[386,1972,1974],{"id":1973},"escape-key-dismissal-patterns","Escape Key & Dismissal Patterns",[324,1976,1977,1978,1980],{},"Standardizing keyboard dismissal requires respecting user intent while preventing accidental data loss. The ",[345,1979,563],{}," key must reliably close non-destructive dialogs without propagating events to parent overlays or route handlers.",[324,1982,1983],{},[335,1984,581],{},[339,1986,1987,1996,1999,2012],{},[342,1988,1989,1990,1992,1993,1995],{},"Bind a ",[345,1991,1058],{}," listener for ",[345,1994,563],{}," at the modal root or document level.",[342,1997,1998],{},"Differentiate between destructive (e.g., form submission) and non-destructive modals before applying auto-close behavior.",[342,2000,2001,2002,549,2005,2008,2009,2011],{},"Use ",[345,2003,2004],{},"e.stopPropagation()",[345,2006,2007],{},"e.preventDefault()"," to prevent ",[345,2010,563],{}," from bubbling to parent modals or triggering unintended route changes.",[342,2013,2014,2015,2018,2019,2022],{},"Provide an explicit close button with a descriptive ",[345,2016,2017],{},"aria-label"," (e.g., ",[345,2020,2021],{},"aria-label=\"Close dialog\"",") to ensure parity for screen reader and pointer users.",[324,2024,2025,2027,2028,2030],{},[335,2026,641],{}," Simulate rapid ",[345,2029,563],{}," presses during framework state transitions and async data fetching. Ensure event listeners are properly cleaned up on unmount to prevent memory leaks and duplicate handler execution.",[650,2032,2034],{"id":2033},"layered-dismissal-and-the-top-of-stack-rule","Layered Dismissal and the Top-of-Stack Rule",[324,2036,2037,2038,2040],{},"When dialogs can stack, only the topmost dialog should respond to ",[345,2039,563],{},". A global document listener that closes every open dialog at once is a common and disorienting bug. Maintain a small stack and let only the last entry handle the key.",[668,2042,2044],{"className":1770,"code":2043,"language":1772,"meta":673,"style":673},"const dialogStack: Array\u003C() => void> = [];\n\nexport function pushDialog(onClose: () => void) {\n  dialogStack.push(onClose);\n  return () => {\n    const i = dialogStack.indexOf(onClose);\n    if (i > -1) dialogStack.splice(i, 1);\n  };\n}\n\ndocument.addEventListener('keydown', (e) => {\n  if (e.key !== 'Escape' || dialogStack.length === 0) return;\n  e.stopPropagation();\n  dialogStack[dialogStack.length - 1]();   \u002F\u002F close only the top dialog\n});\n",[345,2045,2046,2074,2078,2101,2112,2122,2139,2167,2172,2176,2180,2202,2230,2240,2257],{"__ignoreMap":673},[677,2047,2048,2050,2053,2055,2058,2061,2063,2066,2069,2071],{"class":443,"line":679},[677,2049,1113],{"class":682},[677,2051,2052],{"class":751}," dialogStack",[677,2054,1190],{"class":682},[677,2056,2057],{"class":716}," Array",[677,2059,2060],{"class":686},"\u003C() ",[677,2062,790],{"class":682},[677,2064,2065],{"class":751}," void",[677,2067,2068],{"class":686},"> ",[677,2070,911],{"class":682},[677,2072,2073],{"class":686}," [];\n",[677,2075,2076],{"class":443,"line":700},[677,2077,704],{"emptyLinePlaceholder":703},[677,2079,2080,2082,2084,2087,2089,2091,2093,2095,2097,2099],{"class":443,"line":707},[677,2081,710],{"class":682},[677,2083,713],{"class":682},[677,2085,2086],{"class":716}," pushDialog",[677,2088,1165],{"class":686},[677,2090,729],{"class":716},[677,2092,1190],{"class":682},[677,2094,1671],{"class":686},[677,2096,790],{"class":682},[677,2098,2065],{"class":751},[677,2100,1196],{"class":686},[677,2102,2103,2106,2109],{"class":443,"line":745},[677,2104,2105],{"class":686},"  dialogStack.",[677,2107,2108],{"class":716},"push",[677,2110,2111],{"class":686},"(onClose);\n",[677,2113,2114,2116,2118,2120],{"class":443,"line":776},[677,2115,889],{"class":682},[677,2117,1671],{"class":686},[677,2119,790],{"class":682},[677,2121,793],{"class":686},[677,2123,2124,2126,2129,2131,2134,2137],{"class":443,"line":781},[677,2125,799],{"class":682},[677,2127,2128],{"class":751}," i",[677,2130,755],{"class":682},[677,2132,2133],{"class":686}," dialogStack.",[677,2135,2136],{"class":716},"indexOf",[677,2138,2111],{"class":686},[677,2140,2141,2143,2146,2149,2151,2154,2157,2160,2163,2165],{"class":443,"line":796},[677,2142,813],{"class":682},[677,2144,2145],{"class":686}," (i ",[677,2147,2148],{"class":682},">",[677,2150,1425],{"class":682},[677,2152,2153],{"class":751},"1",[677,2155,2156],{"class":686},") dialogStack.",[677,2158,2159],{"class":716},"splice",[677,2161,2162],{"class":686},"(i, ",[677,2164,2153],{"class":751},[677,2166,773],{"class":686},[677,2168,2169],{"class":443,"line":810},[677,2170,2171],{"class":686},"  };\n",[677,2173,2174],{"class":443,"line":830},[677,2175,1009],{"class":686},[677,2177,2178],{"class":443,"line":853},[677,2179,704],{"emptyLinePlaceholder":703},[677,2181,2182,2185,2187,2189,2191,2194,2196,2198,2200],{"class":443,"line":875},[677,2183,2184],{"class":686},"document.",[677,2186,1583],{"class":716},[677,2188,1165],{"class":686},[677,2190,1588],{"class":693},[677,2192,2193],{"class":686},", (",[677,2195,954],{"class":723},[677,2197,957],{"class":686},[677,2199,790],{"class":682},[677,2201,793],{"class":686},[677,2203,2204,2206,2208,2210,2213,2216,2218,2220,2222,2224,2226,2228],{"class":443,"line":881},[677,2205,1810],{"class":682},[677,2207,1307],{"class":686},[677,2209,1310],{"class":682},[677,2211,2212],{"class":693}," 'Escape'",[677,2214,2215],{"class":682}," ||",[677,2217,2133],{"class":686},[677,2219,1375],{"class":751},[677,2221,1378],{"class":682},[677,2223,1381],{"class":751},[677,2225,957],{"class":686},[677,2227,825],{"class":682},[677,2229,697],{"class":686},[677,2231,2232,2235,2238],{"class":443,"line":886},[677,2233,2234],{"class":686},"  e.",[677,2236,2237],{"class":716},"stopPropagation",[677,2239,850],{"class":686},[677,2241,2242,2245,2247,2249,2251,2254],{"class":443,"line":895},[677,2243,2244],{"class":686},"  dialogStack[dialogStack.",[677,2246,1375],{"class":751},[677,2248,1425],{"class":682},[677,2250,1428],{"class":751},[677,2252,2253],{"class":686},"]();   ",[677,2255,2256],{"class":939},"\u002F\u002F close only the top dialog\n",[677,2258,2259],{"class":443,"line":905},[677,2260,2261],{"class":686},"});\n",[324,2263,2264,2265,2267,2268,2270,2271,2274],{},"For an ",[345,2266,1028],{}," guarding unsaved work, the ",[345,2269,563],{}," handler should not close immediately. Instead it should move focus to the cancel-confirmation control or surface a secondary confirmation, honoring ",[345,2272,2273],{},"2.1.2"," (the user can still escape) while protecting against destructive accidental dismissal.",[383,2276],{},[386,2278,2280],{"id":2279},"routing-state-sync-framework-portals","Routing, State Sync & Framework Portals",[324,2282,2283],{},"Modal visibility must align with application state and URL routing to support deep linking, browser history navigation, and SSR hydration consistency. Framework portals are essential for rendering modals outside the DOM hierarchy while preserving logical focus order.",[324,2285,2286],{},[335,2287,581],{},[339,2289,2290,2293,2308,2319],{},[342,2291,2292],{},"Map modal open\u002Fclose states to URL query parameters or hash fragments to enable bookmarkable states.",[342,2294,2295,2296,2299,2300,2303,2304,2307],{},"Render modals via framework-specific portals (e.g., React ",[345,2297,2298],{},"createPortal",", Vue ",[345,2301,2302],{},"\u003CTeleport>",", Angular CDK ",[345,2305,2306],{},"PortalOutlet",") to avoid CSS stacking context and z-index conflicts.",[342,2309,2310,2311,2314,2315,2318],{},"Handle hydration mismatches by deferring focus management until the client-side lifecycle (",[345,2312,2313],{},"useEffect"," \u002F ",[345,2316,2317],{},"onMounted",") executes.",[342,2320,2321],{},"Persist modal state across route transitions if required by complex UX flows, ensuring cleanup occurs on route exit.",[324,2323,2324,2325,2329],{},"When architecting overlay components, evaluate reusable state patterns similar to those used in ",[328,2326,2328],{"href":2327},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits\u002F","Building accessible dropdowns without external UI kits"," to standardize portal rendering and state synchronization.",[324,2331,2332,2334],{},[335,2333,641],{}," Verify that browser back\u002Fforward buttons correctly toggle modal state without triggering hydration warnings or focus jumps. Ensure URL updates are debounced or batched to prevent excessive history entries.",[650,2336,2338],{"id":2337},"vue-3-implementation-teleport-modal-with-lifecycle-focus-restoration","Vue 3 Implementation: Teleport Modal with Lifecycle Focus Restoration",[668,2340,2344],{"className":2341,"code":2342,"language":2343,"meta":673,"style":673},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003CTeleport to=\"body\">\n    \u003Cdiv\n      v-if=\"isOpen\"\n      ref=\"modalContainer\"\n      role=\"dialog\"\n      aria-modal=\"true\"\n      :aria-labelledby=\"titleId\"\n      class=\"modal-overlay\"\n      @keydown.esc=\"handleEscape\"\n    >\n      \u003Cdiv class=\"modal-content\">\n        \u003Ch2 :id=\"titleId\">Confirmation Required\u003C\u002Fh2>\n        \u003Cp>Are you sure you want to proceed?\u003C\u002Fp>\n        \u003Cbutton ref=\"closeBtn\" @click=\"close\">Confirm\u003C\u002Fbutton>\n        \u003Cbutton @click=\"close\">Cancel\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002FTeleport>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, nextTick } from 'vue';\n\nconst props = defineProps\u003C{ isOpen: boolean }>();\nconst emit = defineEmits\u003C{ close: [] }>();\n\nconst modalContainer = ref\u003CHTMLElement | null>(null);\nconst closeBtn = ref\u003CHTMLButtonElement | null>(null);\nconst triggerElement = ref\u003CHTMLElement | null>(null);\nconst titleId = 'modal-title-' + Math.random().toString(36).slice(2, 9);\n\nconst handleEscape = () => emit('close');\n\nconst close = () => {\n  emit('close');\n};\n\nonMounted(() => {\n  if (props.isOpen) {\n    triggerElement.value = document.activeElement as HTMLElement;\n    nextTick(() => {\n      closeBtn.value?.focus();\n    });\n  }\n});\n\nonUnmounted(() => {\n  \u002F\u002F Restore focus only if closing via unmount (e.g., route change)\n  if (triggerElement.value) {\n    triggerElement.value.focus();\n  }\n});\n\u003C\u002Fscript>\n","vue",[345,2345,2346,2355,2373,2380,2390,2399,2409,2419,2429,2439,2449,2453,2471,2493,2506,2534,2553,2562,2570,2579,2588,2592,2612,2626,2630,2654,2675,2679,2704,2730,2755,2805,2809,2831,2835,2850,2861,2866,2870,2880,2887,2902,2913,2922,2927,2931,2935,2939,2950,2955,2962,2971,2975,2979],{"__ignoreMap":673},[677,2347,2348,2350,2353],{"class":443,"line":679},[677,2349,761],{"class":686},[677,2351,2352],{"class":901},"template",[677,2354,997],{"class":686},[677,2356,2357,2360,2363,2366,2368,2371],{"class":443,"line":700},[677,2358,2359],{"class":686},"  \u003C",[677,2361,2362],{"class":901},"Teleport",[677,2364,2365],{"class":716}," to",[677,2367,911],{"class":686},[677,2369,2370],{"class":693},"\"body\"",[677,2372,997],{"class":686},[677,2374,2375,2377],{"class":443,"line":707},[677,2376,898],{"class":686},[677,2378,2379],{"class":901},"div\n",[677,2381,2382,2385,2387],{"class":443,"line":745},[677,2383,2384],{"class":716},"      v-if",[677,2386,911],{"class":686},[677,2388,2389],{"class":693},"\"isOpen\"\n",[677,2391,2392,2394,2396],{"class":443,"line":776},[677,2393,908],{"class":716},[677,2395,911],{"class":686},[677,2397,2398],{"class":693},"\"modalContainer\"\n",[677,2400,2401,2404,2406],{"class":443,"line":781},[677,2402,2403],{"class":716},"      role",[677,2405,911],{"class":686},[677,2407,2408],{"class":693},"\"dialog\"\n",[677,2410,2411,2414,2416],{"class":443,"line":796},[677,2412,2413],{"class":716},"      aria-modal",[677,2415,911],{"class":686},[677,2417,2418],{"class":693},"\"true\"\n",[677,2420,2421,2424,2426],{"class":443,"line":810},[677,2422,2423],{"class":716},"      :aria-labelledby",[677,2425,911],{"class":686},[677,2427,2428],{"class":693},"\"titleId\"\n",[677,2430,2431,2434,2436],{"class":443,"line":830},[677,2432,2433],{"class":716},"      class",[677,2435,911],{"class":686},[677,2437,2438],{"class":693},"\"modal-overlay\"\n",[677,2440,2441,2444,2446],{"class":443,"line":853},[677,2442,2443],{"class":716},"      @keydown.esc",[677,2445,911],{"class":686},[677,2447,2448],{"class":693},"\"handleEscape\"\n",[677,2450,2451],{"class":443,"line":875},[677,2452,979],{"class":686},[677,2454,2455,2458,2461,2464,2466,2469],{"class":443,"line":881},[677,2456,2457],{"class":686},"      \u003C",[677,2459,2460],{"class":901},"div",[677,2462,2463],{"class":716}," class",[677,2465,911],{"class":686},[677,2467,2468],{"class":693},"\"modal-content\"",[677,2470,997],{"class":686},[677,2472,2473,2476,2478,2481,2483,2486,2489,2491],{"class":443,"line":886},[677,2474,2475],{"class":686},"        \u003C",[677,2477,386],{"class":901},[677,2479,2480],{"class":716}," :id",[677,2482,911],{"class":686},[677,2484,2485],{"class":693},"\"titleId\"",[677,2487,2488],{"class":686},">Confirmation Required\u003C\u002F",[677,2490,386],{"class":901},[677,2492,997],{"class":686},[677,2494,2495,2497,2499,2502,2504],{"class":443,"line":895},[677,2496,2475],{"class":686},[677,2498,324],{"class":901},[677,2500,2501],{"class":686},">Are you sure you want to proceed?\u003C\u002F",[677,2503,324],{"class":901},[677,2505,997],{"class":686},[677,2507,2508,2510,2512,2514,2516,2519,2522,2524,2527,2530,2532],{"class":443,"line":905},[677,2509,2475],{"class":686},[677,2511,441],{"class":901},[677,2513,752],{"class":716},[677,2515,911],{"class":686},[677,2517,2518],{"class":693},"\"closeBtn\"",[677,2520,2521],{"class":716}," @click",[677,2523,911],{"class":686},[677,2525,2526],{"class":693},"\"close\"",[677,2528,2529],{"class":686},">Confirm\u003C\u002F",[677,2531,441],{"class":901},[677,2533,997],{"class":686},[677,2535,2536,2538,2540,2542,2544,2546,2549,2551],{"class":443,"line":917},[677,2537,2475],{"class":686},[677,2539,441],{"class":901},[677,2541,2521],{"class":716},[677,2543,911],{"class":686},[677,2545,2526],{"class":693},[677,2547,2548],{"class":686},">Cancel\u003C\u002F",[677,2550,441],{"class":901},[677,2552,997],{"class":686},[677,2554,2555,2558,2560],{"class":443,"line":928},[677,2556,2557],{"class":686},"      \u003C\u002F",[677,2559,2460],{"class":901},[677,2561,997],{"class":686},[677,2563,2564,2566,2568],{"class":443,"line":943},[677,2565,991],{"class":686},[677,2567,2460],{"class":901},[677,2569,997],{"class":686},[677,2571,2572,2575,2577],{"class":443,"line":976},[677,2573,2574],{"class":686},"  \u003C\u002F",[677,2576,2362],{"class":901},[677,2578,997],{"class":686},[677,2580,2581,2584,2586],{"class":443,"line":982},[677,2582,2583],{"class":686},"\u003C\u002F",[677,2585,2352],{"class":901},[677,2587,997],{"class":686},[677,2589,2590],{"class":443,"line":988},[677,2591,704],{"emptyLinePlaceholder":703},[677,2593,2594,2596,2599,2602,2605,2607,2610],{"class":443,"line":1000},[677,2595,761],{"class":686},[677,2597,2598],{"class":901},"script",[677,2600,2601],{"class":716}," setup",[677,2603,2604],{"class":716}," lang",[677,2606,911],{"class":686},[677,2608,2609],{"class":693},"\"ts\"",[677,2611,997],{"class":686},[677,2613,2614,2616,2619,2621,2624],{"class":443,"line":1006},[677,2615,683],{"class":682},[677,2617,2618],{"class":686}," { ref, onMounted, onUnmounted, nextTick } ",[677,2620,690],{"class":682},[677,2622,2623],{"class":693}," 'vue'",[677,2625,697],{"class":686},[677,2627,2628],{"class":443,"line":1437},[677,2629,704],{"emptyLinePlaceholder":703},[677,2631,2632,2634,2637,2639,2642,2645,2647,2649,2651],{"class":443,"line":1456},[677,2633,1113],{"class":682},[677,2635,2636],{"class":751}," props",[677,2638,755],{"class":682},[677,2640,2641],{"class":716}," defineProps",[677,2643,2644],{"class":686},"\u003C{ ",[677,2646,724],{"class":723},[677,2648,1190],{"class":682},[677,2650,1193],{"class":751},[677,2652,2653],{"class":686}," }>();\n",[677,2655,2656,2658,2661,2663,2666,2668,2670,2672],{"class":443,"line":1466},[677,2657,1113],{"class":682},[677,2659,2660],{"class":751}," emit",[677,2662,755],{"class":682},[677,2664,2665],{"class":716}," defineEmits",[677,2667,2644],{"class":686},[677,2669,870],{"class":723},[677,2671,1190],{"class":682},[677,2673,2674],{"class":686}," [] }>();\n",[677,2676,2677],{"class":443,"line":1477},[677,2678,704],{"emptyLinePlaceholder":703},[677,2680,2681,2683,2686,2688,2690,2692,2694,2696,2698,2700,2702],{"class":443,"line":1505},[677,2682,1113],{"class":682},[677,2684,2685],{"class":751}," modalContainer",[677,2687,755],{"class":682},[677,2689,752],{"class":716},[677,2691,761],{"class":686},[677,2693,1234],{"class":716},[677,2695,1237],{"class":682},[677,2697,1240],{"class":751},[677,2699,767],{"class":686},[677,2701,770],{"class":751},[677,2703,773],{"class":686},[677,2705,2706,2708,2711,2713,2715,2717,2720,2722,2724,2726,2728],{"class":443,"line":1514},[677,2707,1113],{"class":682},[677,2709,2710],{"class":751}," closeBtn",[677,2712,755],{"class":682},[677,2714,752],{"class":716},[677,2716,761],{"class":686},[677,2718,2719],{"class":716},"HTMLButtonElement",[677,2721,1237],{"class":682},[677,2723,1240],{"class":751},[677,2725,767],{"class":686},[677,2727,770],{"class":751},[677,2729,773],{"class":686},[677,2731,2732,2734,2737,2739,2741,2743,2745,2747,2749,2751,2753],{"class":443,"line":1524},[677,2733,1113],{"class":682},[677,2735,2736],{"class":751}," triggerElement",[677,2738,755],{"class":682},[677,2740,752],{"class":716},[677,2742,761],{"class":686},[677,2744,1234],{"class":716},[677,2746,1237],{"class":682},[677,2748,1240],{"class":751},[677,2750,767],{"class":686},[677,2752,770],{"class":751},[677,2754,773],{"class":686},[677,2756,2757,2759,2762,2764,2767,2770,2773,2776,2779,2782,2784,2787,2790,2793,2795,2798,2800,2803],{"class":443,"line":1530},[677,2758,1113],{"class":682},[677,2760,2761],{"class":751}," titleId",[677,2763,755],{"class":682},[677,2765,2766],{"class":693}," 'modal-title-'",[677,2768,2769],{"class":682}," +",[677,2771,2772],{"class":686}," Math.",[677,2774,2775],{"class":716},"random",[677,2777,2778],{"class":686},"().",[677,2780,2781],{"class":716},"toString",[677,2783,1165],{"class":686},[677,2785,2786],{"class":751},"36",[677,2788,2789],{"class":686},").",[677,2791,2792],{"class":716},"slice",[677,2794,1165],{"class":686},[677,2796,2797],{"class":751},"2",[677,2799,557],{"class":686},[677,2801,2802],{"class":751},"9",[677,2804,773],{"class":686},[677,2806,2807],{"class":443,"line":1535},[677,2808,704],{"emptyLinePlaceholder":703},[677,2810,2811,2813,2816,2818,2820,2822,2824,2826,2829],{"class":443,"line":1540},[677,2812,1113],{"class":682},[677,2814,2815],{"class":716}," handleEscape",[677,2817,755],{"class":682},[677,2819,1671],{"class":686},[677,2821,790],{"class":682},[677,2823,2660],{"class":716},[677,2825,1165],{"class":686},[677,2827,2828],{"class":693},"'close'",[677,2830,773],{"class":686},[677,2832,2833],{"class":443,"line":1551},[677,2834,704],{"emptyLinePlaceholder":703},[677,2836,2837,2839,2842,2844,2846,2848],{"class":443,"line":1559},[677,2838,1113],{"class":682},[677,2840,2841],{"class":716}," close",[677,2843,755],{"class":682},[677,2845,1671],{"class":686},[677,2847,790],{"class":682},[677,2849,793],{"class":686},[677,2851,2852,2855,2857,2859],{"class":443,"line":1577},[677,2853,2854],{"class":716},"  emit",[677,2856,1165],{"class":686},[677,2858,2828],{"class":693},[677,2860,773],{"class":686},[677,2862,2863],{"class":443,"line":1594},[677,2864,2865],{"class":686},"};\n",[677,2867,2868],{"class":443,"line":1600},[677,2869,704],{"emptyLinePlaceholder":703},[677,2871,2872,2874,2876,2878],{"class":443,"line":1612},[677,2873,2317],{"class":716},[677,2875,787],{"class":686},[677,2877,790],{"class":682},[677,2879,793],{"class":686},[677,2881,2882,2884],{"class":443,"line":1639},[677,2883,1810],{"class":682},[677,2885,2886],{"class":686}," (props.isOpen) {\n",[677,2888,2889,2892,2894,2896,2898,2900],{"class":443,"line":1649},[677,2890,2891],{"class":686},"    triggerElement.value ",[677,2893,911],{"class":682},[677,2895,1447],{"class":686},[677,2897,1569],{"class":682},[677,2899,1572],{"class":716},[677,2901,697],{"class":686},[677,2903,2904,2907,2909,2911],{"class":443,"line":1655},[677,2905,2906],{"class":716},"    nextTick",[677,2908,787],{"class":686},[677,2910,790],{"class":682},[677,2912,793],{"class":686},[677,2914,2915,2918,2920],{"class":443,"line":1660},[677,2916,2917],{"class":686},"      closeBtn.value?.",[677,2919,1472],{"class":716},[677,2921,850],{"class":686},[677,2923,2924],{"class":443,"line":1665},[677,2925,2926],{"class":686},"    });\n",[677,2928,2929],{"class":443,"line":1678},[677,2930,1847],{"class":686},[677,2932,2933],{"class":443,"line":1692},[677,2934,2261],{"class":686},[677,2936,2937],{"class":443,"line":1705},[677,2938,704],{"emptyLinePlaceholder":703},[677,2940,2941,2944,2946,2948],{"class":443,"line":1715},[677,2942,2943],{"class":716},"onUnmounted",[677,2945,787],{"class":686},[677,2947,790],{"class":682},[677,2949,793],{"class":686},[677,2951,2952],{"class":443,"line":1721},[677,2953,2954],{"class":939},"  \u002F\u002F Restore focus only if closing via unmount (e.g., route change)\n",[677,2956,2957,2959],{"class":443,"line":1727},[677,2958,1810],{"class":682},[677,2960,2961],{"class":686}," (triggerElement.value) {\n",[677,2963,2964,2967,2969],{"class":443,"line":1733},[677,2965,2966],{"class":686},"    triggerElement.value.",[677,2968,1472],{"class":716},[677,2970,850],{"class":686},[677,2972,2973],{"class":443,"line":1738},[677,2974,1847],{"class":686},[677,2976,2977],{"class":443,"line":1746},[677,2978,2261],{"class":686},[677,2980,2982,2984,2986],{"class":443,"line":2981},54,[677,2983,2583],{"class":686},[677,2985,2598],{"class":901},[677,2987,997],{"class":686},[650,2989,2991,2992,2994],{"id":2990},"applying-inert-to-sibling-content","Applying ",[345,2993,628],{}," to Sibling Content",[324,2996,2997,2998,3001],{},"A portal renders the dialog as a sibling of ",[345,2999,3000],{},"\u003Cbody>","'s other children rather than a descendant of the trigger. That sibling layout makes it straightforward to mark everything else inert while the dialog is open, which is the most reliable way to hide background content from both the keyboard and the accessibility tree.",[668,3003,3005],{"className":1770,"code":3004,"language":1772,"meta":673,"style":673},"function setBackgroundInert(dialogEl: HTMLElement, on: boolean) {\n  for (const child of Array.from(document.body.children)) {\n    if (child === dialogEl) continue;\n    if (on) child.setAttribute('inert', '');\n    else child.removeAttribute('inert');\n  }\n}\n",[345,3006,3007,3034,3056,3073,3095,3112,3116],{"__ignoreMap":673},[677,3008,3009,3011,3014,3016,3019,3021,3023,3025,3028,3030,3032],{"class":443,"line":679},[677,3010,1779],{"class":682},[677,3012,3013],{"class":716}," setBackgroundInert",[677,3015,1165],{"class":686},[677,3017,3018],{"class":723},"dialogEl",[677,3020,1190],{"class":682},[677,3022,1572],{"class":716},[677,3024,557],{"class":686},[677,3026,3027],{"class":723},"on",[677,3029,1190],{"class":682},[677,3031,1193],{"class":751},[677,3033,1196],{"class":686},[677,3035,3036,3039,3041,3043,3046,3049,3051,3053],{"class":443,"line":700},[677,3037,3038],{"class":682},"  for",[677,3040,816],{"class":686},[677,3042,1113],{"class":682},[677,3044,3045],{"class":751}," child",[677,3047,3048],{"class":682}," of",[677,3050,1335],{"class":686},[677,3052,690],{"class":716},[677,3054,3055],{"class":686},"(document.body.children)) {\n",[677,3057,3058,3060,3063,3065,3068,3071],{"class":443,"line":707},[677,3059,813],{"class":682},[677,3061,3062],{"class":686}," (child ",[677,3064,1450],{"class":682},[677,3066,3067],{"class":686}," dialogEl) ",[677,3069,3070],{"class":682},"continue",[677,3072,697],{"class":686},[677,3074,3075,3077,3080,3083,3085,3088,3090,3093],{"class":443,"line":745},[677,3076,813],{"class":682},[677,3078,3079],{"class":686}," (on) child.",[677,3081,3082],{"class":716},"setAttribute",[677,3084,1165],{"class":686},[677,3086,3087],{"class":693},"'inert'",[677,3089,557],{"class":686},[677,3091,3092],{"class":693},"''",[677,3094,773],{"class":686},[677,3096,3097,3100,3103,3106,3108,3110],{"class":443,"line":776},[677,3098,3099],{"class":682},"    else",[677,3101,3102],{"class":686}," child.",[677,3104,3105],{"class":716},"removeAttribute",[677,3107,1165],{"class":686},[677,3109,3087],{"class":693},[677,3111,773],{"class":686},[677,3113,3114],{"class":443,"line":781},[677,3115,1847],{"class":686},[677,3117,3118],{"class":443,"line":796},[677,3119,1009],{"class":686},[324,3121,3122,3124,3125,3128,3129,3131],{},[345,3123,628],{}," is superior to a manual ",[345,3126,3127],{},"aria-hidden"," sweep because it removes background nodes from the tab order and the accessibility tree simultaneously, in a single declarative attribute. Pair it with the focus trap rather than replacing the trap: ",[345,3130,628],{}," prevents drift outward, while the trap guarantees the cycle stays correct if a third-party script injects a focusable node into the dialog itself.",[383,3133],{},[386,3135,3137],{"id":3136},"common-implementation-pitfalls","Common Implementation Pitfalls",[339,3139,3140,3161,3171,3187,3193,3202,3211],{},[342,3141,3142,3148,3149,557,3152,3155,3156,601,3159,605],{},[335,3143,3144,3145,3147],{},"Using ",[345,3146,1758],{}," for visibility toggling:"," This leaves nodes in the DOM, causing screen readers to parse hidden content. Use conditional rendering (",[345,3150,3151],{},"v-if",[345,3153,3154],{},"{isOpen && ...}",") or ",[345,3157,3158],{},"aria-hidden=\"true\"",[345,3160,628],{},[342,3162,3163,3166,3167,3170],{},[335,3164,3165],{},"Failing to restore trigger focus:"," Breaking spatial memory for keyboard users. Always cache ",[345,3168,3169],{},"document.activeElement"," before opening and restore it synchronously on close.",[342,3172,3173,3181,3182,3184,3185,605],{},[335,3174,3175,3176,3178,3179,1190],{},"Overusing ",[345,3177,3127],{}," without ",[345,3180,628],{}," ",[345,3183,3127],{}," only affects screen readers, not keyboard focus. Background elements remain tabbable unless explicitly trapped or marked ",[345,3186,628],{},[342,3188,3189,3192],{},[335,3190,3191],{},"Ignoring hydration timing:"," Applying focus during SSR or before hydration completes causes layout shifts and focus jumps. Defer all focus operations to client-side mount hooks.",[342,3194,3195,3198,3199,3201],{},[335,3196,3197],{},"Unmanaged portal event listeners:"," Attaching global ",[345,3200,1058],{}," listeners without cleanup on unmount leads to memory leaks and duplicate event handling.",[342,3203,3204,3207,3208,3210],{},[335,3205,3206],{},"Caching focusable boundaries at open time:"," Reactive re-renders mutate the focusable set; recompute it on each ",[345,3209,505],{}," keypress so the trap never redirects to a stale element.",[342,3212,3213,3218],{},[335,3214,3215,3216,1190],{},"Closing every dialog on a single ",[345,3217,563],{}," In stacked overlays, only the topmost dialog should dismiss. Use a dialog stack and let the top entry consume the event.",[383,3220],{},[386,3222,3224],{"id":3223},"how-to-verify-modal-keyboard-accessibility","How to Verify Modal Keyboard Accessibility",[324,3226,3227],{},"Automated tooling catches the structural failures; manual keyboard testing catches the behavioral ones. Use both.",[324,3229,3230,3233,3234,3237,3238,3241,3242,3244,3245,3247],{},[335,3231,3232],{},"Automated:"," Run ",[345,3235,3236],{},"axe-core"," (via ",[345,3239,3240],{},"@axe-core\u002Fplaywright"," or the browser extension) against the open modal. It flags missing ",[345,3243,611],{},", an absent dialog role, and background content that is reachable while ",[345,3246,604],{}," is set. Add a Playwright assertion that drives the keyboard end to end:",[668,3249,3251],{"className":1770,"code":3250,"language":1772,"meta":673,"style":673},"test('modal traps focus and restores it', async ({ page }) => {\n  await page.getByRole('button', { name: 'Open' }).focus();\n  await page.keyboard.press('Enter');\n  await expect(page.getByRole('dialog')).toBeVisible();\n\n  \u002F\u002F Tab past the last element should wrap to the first, not escape.\n  for (let i = 0; i \u003C 8; i++) await page.keyboard.press('Tab');\n  await expect(page.getByRole('dialog')).toContainText(''); \u002F\u002F focus still inside\n  const inside = await page.evaluate(() =>\n    document.querySelector('[role=\"dialog\"]')!.contains(document.activeElement));\n  expect(inside).toBe(true);\n\n  await page.keyboard.press('Escape');\n  await expect(page.getByRole('dialog')).toBeHidden();\n  await expect(page.getByRole('button', { name: 'Open' })).toBeFocused();\n});\n",[345,3252,3253,3281,3310,3327,3352,3356,3361,3407,3436,3458,3482,3500,3504,3519,3540,3566],{"__ignoreMap":673},[677,3254,3255,3258,3260,3263,3265,3268,3271,3274,3277,3279],{"class":443,"line":679},[677,3256,3257],{"class":716},"test",[677,3259,1165],{"class":686},[677,3261,3262],{"class":693},"'modal traps focus and restores it'",[677,3264,557],{"class":686},[677,3266,3267],{"class":682},"async",[677,3269,3270],{"class":686}," ({ ",[677,3272,3273],{"class":723},"page",[677,3275,3276],{"class":686}," }) ",[677,3278,790],{"class":682},[677,3280,793],{"class":686},[677,3282,3283,3286,3289,3292,3294,3297,3300,3303,3306,3308],{"class":443,"line":700},[677,3284,3285],{"class":682},"  await",[677,3287,3288],{"class":686}," page.",[677,3290,3291],{"class":716},"getByRole",[677,3293,1165],{"class":686},[677,3295,3296],{"class":693},"'button'",[677,3298,3299],{"class":686},", { name: ",[677,3301,3302],{"class":693},"'Open'",[677,3304,3305],{"class":686}," }).",[677,3307,1472],{"class":716},[677,3309,850],{"class":686},[677,3311,3312,3314,3317,3320,3322,3325],{"class":443,"line":707},[677,3313,3285],{"class":682},[677,3315,3316],{"class":686}," page.keyboard.",[677,3318,3319],{"class":716},"press",[677,3321,1165],{"class":686},[677,3323,3324],{"class":693},"'Enter'",[677,3326,773],{"class":686},[677,3328,3329,3331,3334,3337,3339,3341,3344,3347,3350],{"class":443,"line":745},[677,3330,3285],{"class":682},[677,3332,3333],{"class":716}," expect",[677,3335,3336],{"class":686},"(page.",[677,3338,3291],{"class":716},[677,3340,1165],{"class":686},[677,3342,3343],{"class":693},"'dialog'",[677,3345,3346],{"class":686},")).",[677,3348,3349],{"class":716},"toBeVisible",[677,3351,850],{"class":686},[677,3353,3354],{"class":443,"line":776},[677,3355,704],{"emptyLinePlaceholder":703},[677,3357,3358],{"class":443,"line":781},[677,3359,3360],{"class":939},"  \u002F\u002F Tab past the last element should wrap to the first, not escape.\n",[677,3362,3363,3365,3367,3370,3373,3375,3377,3380,3382,3385,3388,3391,3393,3396,3398,3400,3402,3405],{"class":443,"line":796},[677,3364,3038],{"class":682},[677,3366,816],{"class":686},[677,3368,3369],{"class":682},"let",[677,3371,3372],{"class":686}," i ",[677,3374,911],{"class":682},[677,3376,1381],{"class":751},[677,3378,3379],{"class":686},"; i ",[677,3381,761],{"class":682},[677,3383,3384],{"class":751}," 8",[677,3386,3387],{"class":686},"; i",[677,3389,3390],{"class":682},"++",[677,3392,957],{"class":686},[677,3394,3395],{"class":682},"await",[677,3397,3316],{"class":686},[677,3399,3319],{"class":716},[677,3401,1165],{"class":686},[677,3403,3404],{"class":693},"'Tab'",[677,3406,773],{"class":686},[677,3408,3409,3411,3413,3415,3417,3419,3421,3423,3426,3428,3430,3433],{"class":443,"line":810},[677,3410,3285],{"class":682},[677,3412,3333],{"class":716},[677,3414,3336],{"class":686},[677,3416,3291],{"class":716},[677,3418,1165],{"class":686},[677,3420,3343],{"class":693},[677,3422,3346],{"class":686},[677,3424,3425],{"class":716},"toContainText",[677,3427,1165],{"class":686},[677,3429,3092],{"class":693},[677,3431,3432],{"class":686},"); ",[677,3434,3435],{"class":939},"\u002F\u002F focus still inside\n",[677,3437,3438,3440,3443,3445,3448,3450,3453,3455],{"class":443,"line":830},[677,3439,748],{"class":682},[677,3441,3442],{"class":751}," inside",[677,3444,755],{"class":682},[677,3446,3447],{"class":682}," await",[677,3449,3288],{"class":686},[677,3451,3452],{"class":716},"evaluate",[677,3454,787],{"class":686},[677,3456,3457],{"class":682},"=>\n",[677,3459,3460,3463,3465,3467,3470,3472,3474,3476,3479],{"class":443,"line":853},[677,3461,3462],{"class":686},"    document.",[677,3464,1626],{"class":716},[677,3466,1165],{"class":686},[677,3468,3469],{"class":693},"'[role=\"dialog\"]'",[677,3471,1794],{"class":686},[677,3473,819],{"class":682},[677,3475,605],{"class":686},[677,3477,3478],{"class":716},"contains",[677,3480,3481],{"class":686},"(document.activeElement));\n",[677,3483,3484,3487,3490,3493,3495,3498],{"class":443,"line":875},[677,3485,3486],{"class":716},"  expect",[677,3488,3489],{"class":686},"(inside).",[677,3491,3492],{"class":716},"toBe",[677,3494,1165],{"class":686},[677,3496,3497],{"class":751},"true",[677,3499,773],{"class":686},[677,3501,3502],{"class":443,"line":881},[677,3503,704],{"emptyLinePlaceholder":703},[677,3505,3506,3508,3510,3512,3514,3517],{"class":443,"line":886},[677,3507,3285],{"class":682},[677,3509,3316],{"class":686},[677,3511,3319],{"class":716},[677,3513,1165],{"class":686},[677,3515,3516],{"class":693},"'Escape'",[677,3518,773],{"class":686},[677,3520,3521,3523,3525,3527,3529,3531,3533,3535,3538],{"class":443,"line":895},[677,3522,3285],{"class":682},[677,3524,3333],{"class":716},[677,3526,3336],{"class":686},[677,3528,3291],{"class":716},[677,3530,1165],{"class":686},[677,3532,3343],{"class":693},[677,3534,3346],{"class":686},[677,3536,3537],{"class":716},"toBeHidden",[677,3539,850],{"class":686},[677,3541,3542,3544,3546,3548,3550,3552,3554,3556,3558,3561,3564],{"class":443,"line":905},[677,3543,3285],{"class":682},[677,3545,3333],{"class":716},[677,3547,3336],{"class":686},[677,3549,3291],{"class":716},[677,3551,1165],{"class":686},[677,3553,3296],{"class":693},[677,3555,3299],{"class":686},[677,3557,3302],{"class":693},[677,3559,3560],{"class":686}," })).",[677,3562,3563],{"class":716},"toBeFocused",[677,3565,850],{"class":686},[677,3567,3568],{"class":443,"line":917},[677,3569,2261],{"class":686},[324,3571,3572,3575,3576,3578,3579,3581,3582,3584],{},[335,3573,3574],{},"Manual:"," Open the modal with the keyboard only, then hold ",[345,3577,505],{}," through a full cycle and confirm focus never lands on background content. Press ",[345,3580,552],{}," from the first element and confirm it wraps to the last. Press ",[345,3583,563],{}," and confirm the dialog closes and focus returns to the trigger. Finally, repeat the entire sequence with a screen reader (NVDA on Windows, VoiceOver on macOS) and confirm the dialog role and accessible name are announced on open, and that the background is not reachable in browse mode.",[383,3586],{},[386,3588,3590],{"id":3589},"frequently-asked-questions","Frequently Asked Questions",[324,3592,3593,3602,3603,3605,3606,3608,3609,3611,3612,549,3614,3616],{},[335,3594,3595,3596,3598,3599,3601],{},"Should I use the native ",[345,3597,589],{}," element or a custom ",[345,3600,2460],{}," with ARIA?","\nPrefer ",[345,3604,589],{}," with ",[345,3607,593],{}," for native focus trapping and ",[345,3610,563],{}," handling. Fall back to ",[345,3613,600],{},[345,3615,604],{}," for legacy browsers or heavily customized implementations. Framework portals typically still require custom focus management regardless of the base element.",[324,3618,3619,3622,3623,3626,3627,3630],{},[335,3620,3621],{},"How do I handle focus when a modal contains a nested scrollable list?","\nUse ",[345,3624,3625],{},"tabindex=\"0\""," on the scroll container and intercept arrow keys to navigate list items without triggering page scroll. Ensure the container receives focus before list traversal begins, and manage ",[345,3628,3629],{},"aria-activedescendant"," for virtualized lists.",[324,3632,3633,3639,3640,3642,3643,3645],{},[335,3634,3635,3636,3638],{},"Does ",[345,3637,604],{}," automatically hide background content from screen readers?","\nNo. ",[345,3641,645],{}," only signals intent to assistive technologies. You must manually apply ",[345,3644,628],{}," to sibling content or implement a strict focus trap to prevent background interaction and traversal.",[324,3647,3648,3651,3652,3654],{},[335,3649,3650],{},"How do I avoid violating WCAG 2.1.2 No Keyboard Trap while still trapping focus?","\nThe criterion forbids traps with no keyboard exit, not the deliberate focus loop of a modal. As long as ",[345,3653,563],{}," (and a visible close button) always dismisses the dialog and returns focus to the page, your cyclic trap is compliant. Provide that exit before shipping the trap.",[324,3656,3657,3663,3664,3666,3667,3669,3670,3672],{},[335,3658,3659,3660,3662],{},"Should ",[345,3661,563],{}," close a modal that contains unsaved form data?","\nFor destructive loss, treat the modal as an ",[345,3665,1028],{}," and intercept ",[345,3668,563],{}," to surface a confirmation rather than closing outright. The user must still be able to leave, so route ",[345,3671,563],{}," to a \"discard or keep editing\" prompt instead of silently destroying input.",[324,3674,3675,3678,3679,3682,3683,3686],{},[335,3676,3677],{},"Where should the focus go when the modal first opens?","\nMove focus to the first meaningful interactive element, or to the dialog container (with ",[345,3680,3681],{},"tabindex=\"-1\"",") when the content is primarily text. Avoid focusing a destructive button by default, since a reflexive ",[345,3684,3685],{},"Enter"," could trigger an irreversible action.",[383,3688],{},[386,3690,3692],{"id":3691},"related-guides","Related guides",[339,3694,3695,3699,3703,3707],{},[342,3696,3697],{},[328,3698,10],{"href":330},[342,3700,3701],{},[328,3702,43],{"href":2327},[342,3704,3705],{},[328,3706,25],{"href":1076},[342,3708,3709],{},[328,3710,3712],{"href":3711},"\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002F","Handling Accessible Modals in Next.js 14 Server Components",[3714,3715,3716],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .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 pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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":673,"searchDepth":700,"depth":700,"links":3718},[3719,3720,3724,3728,3731,3736,3737,3738,3739],{"id":388,"depth":700,"text":389},{"id":572,"depth":700,"text":573,"children":3721},[3722],{"id":652,"depth":707,"text":3723},"Native \u003Cdialog> as a Baseline",{"id":1034,"depth":700,"text":1035,"children":3725},[3726,3727],{"id":1085,"depth":707,"text":1086},{"id":1751,"depth":707,"text":1752},{"id":1973,"depth":700,"text":1974,"children":3729},[3730],{"id":2033,"depth":707,"text":2034},{"id":2279,"depth":700,"text":2280,"children":3732},[3733,3734],{"id":2337,"depth":707,"text":2338},{"id":2990,"depth":707,"text":3735},"Applying inert to Sibling Content",{"id":3136,"depth":700,"text":3137},{"id":3223,"depth":700,"text":3224},{"id":3589,"depth":700,"text":3590},{"id":3691,"depth":700,"text":3692},null,"Master keyboard-accessible modal patterns with reliable focus trapping, escape handling, and ARIA semantics that hold up in real production UI flows.","md",{},false,{"title":37,"description":3741},"YTyM02FsTz9d9exsuGfuCMiIH3zclYaJJy8Aydtek60",[3748,3787,3788,3851],{"title":5,"path":6,"stem":7,"children":3749,"page":-1},[3750,3751,3754,3757,3763,3769,3778,3784],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":3752},[3753],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":3755},[3756],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":3758,"page":-1},[3759,3760],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":3761},[3762],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":3764,"page":-1},[3765,3766],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":3767},[3768],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":3770,"page":-1},[3771,3772,3775],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":3773},[3774],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":3776},[3777],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":3779},[3780,3781],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":3782},[3783],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":3785},[3786],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":3789,"page":-1},[3790,3791,3797,3809,3821,3824,3833,3845],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":3792,"page":-1},[3793,3794],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":3795},[3796],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":3798,"page":-1},[3799,3800,3803,3806],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":3801},[3802],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":3804},[3805],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":3807},[3808],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":3810,"page":-1},[3811,3812,3815,3818],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":3813},[3814],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":3816},[3817],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":3819},[3820],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":3822},[3823],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":3825,"page":-1},[3826,3827,3830],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":3828},[3829],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":3831},[3832],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":3834,"page":-1},[3835,3836,3839,3842],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":3837},[3838],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":3840},[3841],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":3843},[3844],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":3846,"page":-1},[3847,3848],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":3849},[3850],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":3852,"page":-1},[3853,3854,3863,3872,3881,3890],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":3855},[3856,3857,3860],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":3858},[3859],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":3861},[3862],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":3864,"page":-1},[3865,3866,3869],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":3867},[3868],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":3870},[3871],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":3873,"page":-1},[3874,3875,3878],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":3876},[3877],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":3879},[3880],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":3882,"page":-1},[3883,3884,3887],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":3885},[3886],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":3888},[3889],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":3891,"page":-1},[3892,3893,3896],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":3894},[3895],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":3897},[3898],{"title":309,"path":310,"stem":311},1781785523210]