[{"data":1,"prerenderedAt":3190},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002F":314,"content-navigation":3038},[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":205,"body":316,"date":3031,"description":3032,"extension":3033,"image":3031,"meta":3034,"modifiedAt":3031,"navigation":620,"noindex":3035,"path":206,"publishedAt":3031,"seo":3036,"stem":207,"updatedAt":3031,"__hash__":3037},"content\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex.md",{"type":317,"value":318,"toc":3014},"minimark",[319,323,332,338,363,368,386,391,414,525,532,536,544,549,577,813,819,824,842,1005,1008,1012,1015,1019,1055,1345,1353,1361,1371,1644,1651,1655,1658,1662,1696,2616,2621,2625,2632,2861,2865,2913,2917,2938,2942,2951,2960,2969,2975,2988,2992,3010],[320,321,205],"h1",{"id":322},"server-components-client-side-interactivity",[324,325,326,327,331],"p",{},"Modern frameworks split rendering between server and client environments. While foundational principles are covered in ",[328,329,94],"a",{"href":330},"\u002Freact-nextjs-accessibility-patterns\u002F",", managing interactive elements across this boundary requires careful state and focus handling. React Server Components (RSC) serialize to static HTML, meaning they cannot execute browser APIs, manage focus, or attach event listeners directly. This guide bridges server-rendered markup with client-side interactivity without compromising accessibility, ensuring seamless experiences across the hydration boundary.",[324,333,334],{},[335,336,337],"strong",{},"Mapped WCAG 2.2 Criteria:",[339,340,341,348,353,358],"ul",{},[342,343,344],"li",{},[345,346,347],"code",{},"1.3.1 Info and Relationships",[342,349,350],{},[345,351,352],{},"2.1.1 Keyboard",[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],{},"Implementation Key Points:",[339,369,370,373,380,383],{},[342,371,372],{},"Understand RSC serialization limits for interactive state",[342,374,375,376,379],{},"Implement ",[345,377,378],{},"'use client'"," boundaries strategically at component leaf nodes",[342,381,382],{},"Maintain focus and ARIA state across hydration",[342,384,385],{},"Leverage progressive enhancement for critical interactions",[387,388,390],"h2",{"id":389},"where-accessibility-lives-in-the-rsc-model","Where Accessibility Lives in the RSC Model",[324,392,393,394,397,398,401,402,406,407,410,411,413],{},"The single most useful mental model for accessible RSC applications is knowing which obligations belong to the server and which belong to the client. Semantic structure—landmarks, headings, labels, the initial ",[345,395,396],{},"role"," and ",[345,399,400],{},"aria-*"," attributes—is cheap to render on the server and should be present in the first byte of HTML so that assistive technology has a complete document even before any JavaScript runs. ",[403,404,405],"em",{},"Dynamic"," behavior—focus movement, keyboard traps, ",[345,408,409],{},"aria-expanded"," toggling, live region announcements—can only happen on the client, behind a ",[345,412,378],{}," boundary. The diagram below shows the split and the hydration risks that appear when an attribute that must be interactive is rendered statically (or vice versa).",[415,416,423,424,423,428,423,432,423,440,423,456,423,480,423,505,423,512,423,516,423,520],"svg",{"role":417,"ariaLabelledBy":418,"viewBox":421,"style":422},"img",[419,420],"rscTitle","rscDesc","0 0 760 320","width:100%;height:auto;max-width:760px","\n  ",[425,426,427],"title",{"id":419},"The server \u002F client accessibility boundary",[429,430,431],"desc",{"id":420},"Server Components render semantic structure and static ARIA into HTML, while a use client boundary owns focus, keyboard handling, and dynamic ARIA state. Mismatched attributes across the boundary cause hydration errors.",[433,434],"line",{"style":435,"x1":436,"y1":437,"x2":436,"y2":438,"stroke":439},"stroke-width:2;stroke-dasharray:6 6","380","20","300","currentColor",[441,442,445,446,445,452,423],"g",{"style":443,"fill":444},"font-size:13px;text-anchor:middle","var(--muted)","\n    ",[447,448,451],"text",{"x":449,"y":450},"190","40","Server Component",[447,453,455],{"x":454,"y":450},"572","'use client' boundary",[441,457,445,460,445,467,445,470,445,473,445,476,445,478,423],{"style":458,"fill":459,"stroke":439},"stroke-width:2","var(--surface)",[461,462],"rect",{"x":450,"y":463,"width":438,"height":464,"rx":465,"fill":466},"60","44","6","var(--primary-soft)",[461,468],{"x":450,"y":469,"width":438,"height":464,"rx":465,"fill":466},"118",[461,471],{"x":450,"y":472,"width":438,"height":464,"rx":465,"fill":466},"176",[461,474],{"x":475,"y":463,"width":438,"height":464,"rx":465},"420",[461,477],{"x":475,"y":469,"width":438,"height":464,"rx":465},[461,479],{"x":475,"y":472,"width":438,"height":464,"rx":465},[441,481,445,483,445,487,445,491,445,495,445,499,445,502,423],{"style":443,"fill":482},"var(--text)",[447,484,486],{"x":449,"y":485},"87","landmarks & headings",[447,488,490],{"x":449,"y":489},"145","labels & static ARIA",[447,492,494],{"x":449,"y":493},"203","server-safe defaults",[447,496,498],{"x":497,"y":485},"570","focus() & restoration",[447,500,501],{"x":497,"y":489},"keyboard \u002F Tab trap",[447,503,504],{"x":497,"y":493},"aria-expanded toggles",[441,506,445,508,423],{"style":458,"stroke":439,"fill":507},"none",[509,510],"path",{"d":511},"M340 198 L420 198",[513,514],"polygon",{"points":515,"fill":439},"420,198 410,194 410,202",[447,517,519],{"style":443,"x":436,"y":518,"fill":482},"262","serialized props (strings, numbers, plain objects)",[447,521,524],{"style":522,"x":436,"y":523,"fill":444},"font-size:12px;text-anchor:middle","292","attribute mismatch across the line → hydration error",[324,526,527,528,531],{},"Read the boundary line as a contract: anything that must change in response to user input crosses to the right, and it must hydrate from a ",[403,529,530],{},"server-safe default"," on the left so the first paint and the first client render agree.",[387,533,535],{"id":534},"architecting-the-clientserver-boundary","Architecting the Client\u002FServer Boundary",[324,537,538,539,543],{},"The Next.js App Router enforces a strict separation between server-rendered markup and interactive client logic. When designing accessible interfaces, you must isolate interactive components while preserving semantic HTML from the server. As outlined in ",[328,540,542],{"href":541},"\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002F","Next.js App Router & A11y",", routing context and layout hydration rely on predictable DOM structures.",[324,545,546],{},[335,547,548],{},"Key Constraints & Patterns:",[339,550,551,557,560,563],{},[342,552,553,554,556],{},"Apply ",[345,555,378],{}," directives only at leaf nodes to minimize client-side JavaScript bundles.",[342,558,559],{},"Pass serialized props (strings, numbers, plain objects) instead of functions across the boundary.",[342,561,562],{},"Ensure server-rendered fallbacks remain fully keyboard accessible before hydration completes.",[342,564,565,566,569,570,569,573,576],{},"Validate that hydration boundaries do not strip semantic landmarks (",[345,567,568],{},"\u003Cmain>",", ",[345,571,572],{},"\u003Cnav>",[345,574,575],{},"\u003Csection>",").",[578,579,584],"pre",{"className":580,"code":581,"language":582,"meta":583,"style":583},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F components\u002FServerWrapper.tsx (Server Component - Default)\nimport { InteractiveCard } from '.\u002FInteractiveCard';\n\nexport default async function ServerWrapper({ data }: { data: { id: string; title: string } }) {\n  \u002F\u002F Server fetches data, passes serialized props to client boundary\n  return (\n    \u003Carticle aria-labelledby={`card-title-${data.id}`}>\n      \u003CInteractiveCard\n        id={data.id}\n        title={data.title}\n        initialFocus={false} \u002F\u002F Serialized boolean for client initialization\n      \u002F>\n    \u003C\u002Farticle>\n  );\n}\n","tsx","",[345,585,586,594,615,622,684,690,699,734,743,754,765,784,790,801,807],{"__ignoreMap":583},[587,588,590],"span",{"class":433,"line":589},1,[587,591,593],{"class":592},"sJ8bj","\u002F\u002F components\u002FServerWrapper.tsx (Server Component - Default)\n",[587,595,597,601,605,608,612],{"class":433,"line":596},2,[587,598,600],{"class":599},"szBVR","import",[587,602,604],{"class":603},"sVt8B"," { InteractiveCard } ",[587,606,607],{"class":599},"from",[587,609,611],{"class":610},"sZZnC"," '.\u002FInteractiveCard'",[587,613,614],{"class":603},";\n",[587,616,618],{"class":433,"line":617},3,[587,619,621],{"emptyLinePlaceholder":620},true,"\n",[587,623,625,628,631,634,637,641,644,648,651,654,657,659,661,663,666,668,672,675,677,679,681],{"class":433,"line":624},4,[587,626,627],{"class":599},"export",[587,629,630],{"class":599}," default",[587,632,633],{"class":599}," async",[587,635,636],{"class":599}," function",[587,638,640],{"class":639},"sScJk"," ServerWrapper",[587,642,643],{"class":603},"({ ",[587,645,647],{"class":646},"s4XuR","data",[587,649,650],{"class":603}," }",[587,652,653],{"class":599},":",[587,655,656],{"class":603}," { ",[587,658,647],{"class":646},[587,660,653],{"class":599},[587,662,656],{"class":603},[587,664,665],{"class":646},"id",[587,667,653],{"class":599},[587,669,671],{"class":670},"sj4cs"," string",[587,673,674],{"class":603},"; ",[587,676,425],{"class":646},[587,678,653],{"class":599},[587,680,671],{"class":670},[587,682,683],{"class":603}," } }) {\n",[587,685,687],{"class":433,"line":686},5,[587,688,689],{"class":592},"  \u002F\u002F Server fetches data, passes serialized props to client boundary\n",[587,691,693,696],{"class":433,"line":692},6,[587,694,695],{"class":599},"  return",[587,697,698],{"class":603}," (\n",[587,700,702,705,709,712,715,718,721,723,726,728,731],{"class":433,"line":701},7,[587,703,704],{"class":603},"    \u003C",[587,706,708],{"class":707},"s9eBZ","article",[587,710,711],{"class":639}," aria-labelledby",[587,713,714],{"class":599},"=",[587,716,717],{"class":603},"{",[587,719,720],{"class":610},"`card-title-${",[587,722,647],{"class":603},[587,724,725],{"class":610},".",[587,727,665],{"class":603},[587,729,730],{"class":610},"}`",[587,732,733],{"class":603},"}>\n",[587,735,737,740],{"class":433,"line":736},8,[587,738,739],{"class":603},"      \u003C",[587,741,742],{"class":670},"InteractiveCard\n",[587,744,746,749,751],{"class":433,"line":745},9,[587,747,748],{"class":639},"        id",[587,750,714],{"class":599},[587,752,753],{"class":603},"{data.id}\n",[587,755,757,760,762],{"class":433,"line":756},10,[587,758,759],{"class":639},"        title",[587,761,714],{"class":599},[587,763,764],{"class":603},"{data.title}\n",[587,766,768,771,773,775,778,781],{"class":433,"line":767},11,[587,769,770],{"class":639},"        initialFocus",[587,772,714],{"class":599},[587,774,717],{"class":603},[587,776,777],{"class":670},"false",[587,779,780],{"class":603},"} ",[587,782,783],{"class":592},"\u002F\u002F Serialized boolean for client initialization\n",[587,785,787],{"class":433,"line":786},12,[587,788,789],{"class":603},"      \u002F>\n",[587,791,793,796,798],{"class":433,"line":792},13,[587,794,795],{"class":603},"    \u003C\u002F",[587,797,708],{"class":707},[587,799,800],{"class":603},">\n",[587,802,804],{"class":433,"line":803},14,[587,805,806],{"class":603},"  );\n",[587,808,810],{"class":433,"line":809},15,[587,811,812],{"class":603},"}\n",[324,814,815,818],{},[335,816,817],{},"Testing Hook:"," Verify DOM structure matches server output before hydration using React DevTools and axe DevTools. Inspect the network waterfall to ensure client components only hydrate when scrolled into view or explicitly requested.",[820,821,823],"h3",{"id":822},"keeping-the-static-shell-operable","Keeping the Static Shell Operable",[324,825,826,827,830,831,834,835,838,839,841],{},"A subtle accessibility advantage of RSC is that the server-rendered shell is ",[403,828,829],{},"already operable"," for plain links and native form submissions before a single byte of client JavaScript executes. Lean into this: render navigation as real ",[345,832,833],{},"\u003Ca>"," elements and forms as real ",[345,836,837],{},"\u003Cform action={serverAction}>"," so that keyboard users and assistive technology have a working interface during the hydration gap. Reserve the ",[345,840,378],{}," boundary for behaviors that genuinely cannot degrade—focus trapping, live announcements, optimistic UI—rather than re-implementing links and buttons that the platform already makes accessible.",[578,843,845],{"className":580,"code":844,"language":582,"meta":583,"style":583},"\u002F\u002F Progressive enhancement: works without JS, enhanced with it\nimport { subscribe } from '.\u002Factions';\n\nexport default function NewsletterForm() {\n  return (\n    \u003Cform action={subscribe}>\n      \u003Clabel htmlFor=\"email\">Email address\u003C\u002Flabel>\n      \u003Cinput id=\"email\" name=\"email\" type=\"email\" required autoComplete=\"email\" \u002F>\n      \u003Cbutton type=\"submit\">Subscribe\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n",[345,846,847,852,866,870,884,890,905,927,968,989,997,1001],{"__ignoreMap":583},[587,848,849],{"class":433,"line":589},[587,850,851],{"class":592},"\u002F\u002F Progressive enhancement: works without JS, enhanced with it\n",[587,853,854,856,859,861,864],{"class":433,"line":596},[587,855,600],{"class":599},[587,857,858],{"class":603}," { subscribe } ",[587,860,607],{"class":599},[587,862,863],{"class":610}," '.\u002Factions'",[587,865,614],{"class":603},[587,867,868],{"class":433,"line":617},[587,869,621],{"emptyLinePlaceholder":620},[587,871,872,874,876,878,881],{"class":433,"line":624},[587,873,627],{"class":599},[587,875,630],{"class":599},[587,877,636],{"class":599},[587,879,880],{"class":639}," NewsletterForm",[587,882,883],{"class":603},"() {\n",[587,885,886,888],{"class":433,"line":686},[587,887,695],{"class":599},[587,889,698],{"class":603},[587,891,892,894,897,900,902],{"class":433,"line":692},[587,893,704],{"class":603},[587,895,896],{"class":707},"form",[587,898,899],{"class":639}," action",[587,901,714],{"class":599},[587,903,904],{"class":603},"{subscribe}>\n",[587,906,907,909,912,915,917,920,923,925],{"class":433,"line":701},[587,908,739],{"class":603},[587,910,911],{"class":707},"label",[587,913,914],{"class":639}," htmlFor",[587,916,714],{"class":599},[587,918,919],{"class":610},"\"email\"",[587,921,922],{"class":603},">Email address\u003C\u002F",[587,924,911],{"class":707},[587,926,800],{"class":603},[587,928,929,931,934,937,939,941,944,946,948,951,953,955,958,961,963,965],{"class":433,"line":736},[587,930,739],{"class":603},[587,932,933],{"class":707},"input",[587,935,936],{"class":639}," id",[587,938,714],{"class":599},[587,940,919],{"class":610},[587,942,943],{"class":639}," name",[587,945,714],{"class":599},[587,947,919],{"class":610},[587,949,950],{"class":639}," type",[587,952,714],{"class":599},[587,954,919],{"class":610},[587,956,957],{"class":639}," required",[587,959,960],{"class":639}," autoComplete",[587,962,714],{"class":599},[587,964,919],{"class":610},[587,966,967],{"class":603}," \u002F>\n",[587,969,970,972,975,977,979,982,985,987],{"class":433,"line":745},[587,971,739],{"class":603},[587,973,974],{"class":707},"button",[587,976,950],{"class":639},[587,978,714],{"class":599},[587,980,981],{"class":610},"\"submit\"",[587,983,984],{"class":603},">Subscribe\u003C\u002F",[587,986,974],{"class":707},[587,988,800],{"class":603},[587,990,991,993,995],{"class":433,"line":756},[587,992,795],{"class":603},[587,994,896],{"class":707},[587,996,800],{"class":603},[587,998,999],{"class":433,"line":767},[587,1000,806],{"class":603},[587,1002,1003],{"class":433,"line":786},[587,1004,812],{"class":603},[324,1006,1007],{},"Because this is a Server Component using a Server Action, the form submits and the user receives feedback even if hydration never completes—a meaningful resilience win for users on slow or scripting-restricted environments.",[387,1009,1011],{"id":1010},"state-synchronization-aria-updates","State Synchronization & ARIA Updates",[324,1013,1014],{},"Client-side state changes must propagate to ARIA attributes without breaking screen reader announcements. React's concurrent rendering can cause state thrashing during initial hydration if not carefully managed. Dynamic attributes should be initialized with server-safe defaults and updated only after the component mounts.",[324,1016,1017],{},[335,1018,548],{},[339,1020,1021,1028,1042,1048],{},[342,1022,1023,1024,1027],{},"Use ",[345,1025,1026],{},"useEffect"," for post-hydration focus management to avoid hydration mismatches.",[342,1029,1030,1031,1034,1035,86,1038,1041],{},"Bind dynamic ",[345,1032,1033],{},"aria-live"," regions to client state, ensuring ",[345,1036,1037],{},"polite",[345,1039,1040],{},"assertive"," levels match the urgency of updates.",[342,1043,1044,1045,725],{},"Avoid state thrashing during initial render by deferring non-critical updates until ",[345,1046,1047],{},"requestAnimationFrame",[342,1049,1050,1051,1054],{},"Integrate reusable patterns from ",[328,1052,181],{"href":1053},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F"," to standardize state synchronization.",[578,1056,1058],{"className":580,"code":1057,"language":582,"meta":583,"style":583},"\u002F\u002F hooks\u002FuseLiveAnnouncer.ts\n'use client';\nimport { useState, useEffect } from 'react';\n\nexport function useLiveAnnouncer() {\n  const [announcement, setAnnouncement] = useState\u003Cstring>('');\n\n  useEffect(() => {\n    \u002F\u002F Defer announcement until after hydration to prevent SSR\u002Fclient mismatch\n    if (announcement) {\n      const timeout = setTimeout(() => setAnnouncement(''), 1000);\n      return () => clearTimeout(timeout);\n    }\n  }, [announcement]);\n\n  return {\n    announce: (message: string) => setAnnouncement(message),\n    ariaLiveProps: {\n      'aria-live': 'polite' as const,\n      'aria-atomic': true,\n      role: 'status' as const,\n    },\n    announcement,\n  };\n}\n",[345,1059,1060,1065,1071,1085,1089,1100,1139,1143,1157,1162,1170,1204,1220,1225,1230,1234,1241,1267,1273,1294,1307,1322,1328,1334,1340],{"__ignoreMap":583},[587,1061,1062],{"class":433,"line":589},[587,1063,1064],{"class":592},"\u002F\u002F hooks\u002FuseLiveAnnouncer.ts\n",[587,1066,1067,1069],{"class":433,"line":596},[587,1068,378],{"class":610},[587,1070,614],{"class":603},[587,1072,1073,1075,1078,1080,1083],{"class":433,"line":617},[587,1074,600],{"class":599},[587,1076,1077],{"class":603}," { useState, useEffect } ",[587,1079,607],{"class":599},[587,1081,1082],{"class":610}," 'react'",[587,1084,614],{"class":603},[587,1086,1087],{"class":433,"line":624},[587,1088,621],{"emptyLinePlaceholder":620},[587,1090,1091,1093,1095,1098],{"class":433,"line":686},[587,1092,627],{"class":599},[587,1094,636],{"class":599},[587,1096,1097],{"class":639}," useLiveAnnouncer",[587,1099,883],{"class":603},[587,1101,1102,1105,1108,1111,1113,1116,1119,1121,1124,1127,1130,1133,1136],{"class":433,"line":692},[587,1103,1104],{"class":599},"  const",[587,1106,1107],{"class":603}," [",[587,1109,1110],{"class":670},"announcement",[587,1112,569],{"class":603},[587,1114,1115],{"class":670},"setAnnouncement",[587,1117,1118],{"class":603},"] ",[587,1120,714],{"class":599},[587,1122,1123],{"class":639}," useState",[587,1125,1126],{"class":603},"\u003C",[587,1128,1129],{"class":670},"string",[587,1131,1132],{"class":603},">(",[587,1134,1135],{"class":610},"''",[587,1137,1138],{"class":603},");\n",[587,1140,1141],{"class":433,"line":701},[587,1142,621],{"emptyLinePlaceholder":620},[587,1144,1145,1148,1151,1154],{"class":433,"line":736},[587,1146,1147],{"class":639},"  useEffect",[587,1149,1150],{"class":603},"(() ",[587,1152,1153],{"class":599},"=>",[587,1155,1156],{"class":603}," {\n",[587,1158,1159],{"class":433,"line":745},[587,1160,1161],{"class":592},"    \u002F\u002F Defer announcement until after hydration to prevent SSR\u002Fclient mismatch\n",[587,1163,1164,1167],{"class":433,"line":756},[587,1165,1166],{"class":599},"    if",[587,1168,1169],{"class":603}," (announcement) {\n",[587,1171,1172,1175,1178,1181,1184,1186,1188,1191,1194,1196,1199,1202],{"class":433,"line":767},[587,1173,1174],{"class":599},"      const",[587,1176,1177],{"class":670}," timeout",[587,1179,1180],{"class":599}," =",[587,1182,1183],{"class":639}," setTimeout",[587,1185,1150],{"class":603},[587,1187,1153],{"class":599},[587,1189,1190],{"class":639}," setAnnouncement",[587,1192,1193],{"class":603},"(",[587,1195,1135],{"class":610},[587,1197,1198],{"class":603},"), ",[587,1200,1201],{"class":670},"1000",[587,1203,1138],{"class":603},[587,1205,1206,1209,1212,1214,1217],{"class":433,"line":786},[587,1207,1208],{"class":599},"      return",[587,1210,1211],{"class":603}," () ",[587,1213,1153],{"class":599},[587,1215,1216],{"class":639}," clearTimeout",[587,1218,1219],{"class":603},"(timeout);\n",[587,1221,1222],{"class":433,"line":792},[587,1223,1224],{"class":603},"    }\n",[587,1226,1227],{"class":433,"line":803},[587,1228,1229],{"class":603},"  }, [announcement]);\n",[587,1231,1232],{"class":433,"line":809},[587,1233,621],{"emptyLinePlaceholder":620},[587,1235,1237,1239],{"class":433,"line":1236},16,[587,1238,695],{"class":599},[587,1240,1156],{"class":603},[587,1242,1244,1247,1250,1253,1255,1257,1260,1262,1264],{"class":433,"line":1243},17,[587,1245,1246],{"class":639},"    announce",[587,1248,1249],{"class":603},": (",[587,1251,1252],{"class":646},"message",[587,1254,653],{"class":599},[587,1256,671],{"class":670},[587,1258,1259],{"class":603},") ",[587,1261,1153],{"class":599},[587,1263,1190],{"class":639},[587,1265,1266],{"class":603},"(message),\n",[587,1268,1270],{"class":433,"line":1269},18,[587,1271,1272],{"class":603},"    ariaLiveProps: {\n",[587,1274,1276,1279,1282,1285,1288,1291],{"class":433,"line":1275},19,[587,1277,1278],{"class":610},"      'aria-live'",[587,1280,1281],{"class":603},": ",[587,1283,1284],{"class":610},"'polite'",[587,1286,1287],{"class":599}," as",[587,1289,1290],{"class":599}," const",[587,1292,1293],{"class":603},",\n",[587,1295,1297,1300,1302,1305],{"class":433,"line":1296},20,[587,1298,1299],{"class":610},"      'aria-atomic'",[587,1301,1281],{"class":603},[587,1303,1304],{"class":670},"true",[587,1306,1293],{"class":603},[587,1308,1310,1313,1316,1318,1320],{"class":433,"line":1309},21,[587,1311,1312],{"class":603},"      role: ",[587,1314,1315],{"class":610},"'status'",[587,1317,1287],{"class":599},[587,1319,1290],{"class":599},[587,1321,1293],{"class":603},[587,1323,1325],{"class":433,"line":1324},22,[587,1326,1327],{"class":603},"    },\n",[587,1329,1331],{"class":433,"line":1330},23,[587,1332,1333],{"class":603},"    announcement,\n",[587,1335,1337],{"class":433,"line":1336},24,[587,1338,1339],{"class":603},"  };\n",[587,1341,1343],{"class":433,"line":1342},25,[587,1344,812],{"class":603},[324,1346,1347,1349,1350,1352],{},[335,1348,817],{}," Test with VoiceOver\u002FNVDA to confirm live region announcements trigger correctly on state transitions. Use browser accessibility inspectors to verify ",[345,1351,1033],{}," nodes are not being scrubbed by hydration mismatches.",[820,1354,1356,1357,1360],{"id":1355},"avoiding-the-suppresshydrationwarning-trap","Avoiding the ",[345,1358,1359],{},"suppressHydrationWarning"," Trap",[324,1362,1363,1364,1366,1367,1370],{},"When a dynamic ARIA attribute differs between server and client, React logs a hydration warning. The wrong fix is to silence it with ",[345,1365,1359],{},"; that hides a real divergence and can leave assistive technology reading a stale state. The correct fix is to render the attribute's ",[403,1368,1369],{},"resting"," value on the server and transition it on the client after mount:",[578,1372,1374],{"className":580,"code":1373,"language":582,"meta":583,"style":583},"'use client';\nimport { useState, useId } from 'react';\n\nexport function Disclosure({ summary, children }: { summary: string; children: React.ReactNode }) {\n  \u002F\u002F Server-safe default: collapsed. Matches first client render exactly.\n  const [open, setOpen] = useState(false);\n  const panelId = useId();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton\n        aria-expanded={open}\n        aria-controls={panelId}\n        onClick={() => setOpen((v) => !v)}\n      >\n        {summary}\n      \u003C\u002Fbutton>\n      \u003Cdiv id={panelId} hidden={!open}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n}\n",[345,1375,1376,1382,1395,1399,1447,1452,1478,1493,1497,1503,1512,1519,1529,1539,1570,1575,1580,1589,1615,1620,1628,1636,1640],{"__ignoreMap":583},[587,1377,1378,1380],{"class":433,"line":589},[587,1379,378],{"class":610},[587,1381,614],{"class":603},[587,1383,1384,1386,1389,1391,1393],{"class":433,"line":596},[587,1385,600],{"class":599},[587,1387,1388],{"class":603}," { useState, useId } ",[587,1390,607],{"class":599},[587,1392,1082],{"class":610},[587,1394,614],{"class":603},[587,1396,1397],{"class":433,"line":617},[587,1398,621],{"emptyLinePlaceholder":620},[587,1400,1401,1403,1405,1408,1410,1413,1415,1418,1420,1422,1424,1426,1428,1430,1432,1434,1436,1439,1441,1444],{"class":433,"line":624},[587,1402,627],{"class":599},[587,1404,636],{"class":599},[587,1406,1407],{"class":639}," Disclosure",[587,1409,643],{"class":603},[587,1411,1412],{"class":646},"summary",[587,1414,569],{"class":603},[587,1416,1417],{"class":646},"children",[587,1419,650],{"class":603},[587,1421,653],{"class":599},[587,1423,656],{"class":603},[587,1425,1412],{"class":646},[587,1427,653],{"class":599},[587,1429,671],{"class":670},[587,1431,674],{"class":603},[587,1433,1417],{"class":646},[587,1435,653],{"class":599},[587,1437,1438],{"class":639}," React",[587,1440,725],{"class":603},[587,1442,1443],{"class":639},"ReactNode",[587,1445,1446],{"class":603}," }) {\n",[587,1448,1449],{"class":433,"line":686},[587,1450,1451],{"class":592},"  \u002F\u002F Server-safe default: collapsed. Matches first client render exactly.\n",[587,1453,1454,1456,1458,1461,1463,1466,1468,1470,1472,1474,1476],{"class":433,"line":692},[587,1455,1104],{"class":599},[587,1457,1107],{"class":603},[587,1459,1460],{"class":670},"open",[587,1462,569],{"class":603},[587,1464,1465],{"class":670},"setOpen",[587,1467,1118],{"class":603},[587,1469,714],{"class":599},[587,1471,1123],{"class":639},[587,1473,1193],{"class":603},[587,1475,777],{"class":670},[587,1477,1138],{"class":603},[587,1479,1480,1482,1485,1487,1490],{"class":433,"line":701},[587,1481,1104],{"class":599},[587,1483,1484],{"class":670}," panelId",[587,1486,1180],{"class":599},[587,1488,1489],{"class":639}," useId",[587,1491,1492],{"class":603},"();\n",[587,1494,1495],{"class":433,"line":736},[587,1496,621],{"emptyLinePlaceholder":620},[587,1498,1499,1501],{"class":433,"line":745},[587,1500,695],{"class":599},[587,1502,698],{"class":603},[587,1504,1505,1507,1510],{"class":433,"line":756},[587,1506,704],{"class":603},[587,1508,1509],{"class":707},"div",[587,1511,800],{"class":603},[587,1513,1514,1516],{"class":433,"line":767},[587,1515,739],{"class":603},[587,1517,1518],{"class":707},"button\n",[587,1520,1521,1524,1526],{"class":433,"line":786},[587,1522,1523],{"class":639},"        aria-expanded",[587,1525,714],{"class":599},[587,1527,1528],{"class":603},"{open}\n",[587,1530,1531,1534,1536],{"class":433,"line":792},[587,1532,1533],{"class":639},"        aria-controls",[587,1535,714],{"class":599},[587,1537,1538],{"class":603},"{panelId}\n",[587,1540,1541,1544,1546,1549,1551,1554,1557,1560,1562,1564,1567],{"class":433,"line":803},[587,1542,1543],{"class":639},"        onClick",[587,1545,714],{"class":599},[587,1547,1548],{"class":603},"{() ",[587,1550,1153],{"class":599},[587,1552,1553],{"class":639}," setOpen",[587,1555,1556],{"class":603},"((",[587,1558,1559],{"class":646},"v",[587,1561,1259],{"class":603},[587,1563,1153],{"class":599},[587,1565,1566],{"class":599}," !",[587,1568,1569],{"class":603},"v)}\n",[587,1571,1572],{"class":433,"line":809},[587,1573,1574],{"class":603},"      >\n",[587,1576,1577],{"class":433,"line":1236},[587,1578,1579],{"class":603},"        {summary}\n",[587,1581,1582,1585,1587],{"class":433,"line":1243},[587,1583,1584],{"class":603},"      \u003C\u002F",[587,1586,974],{"class":707},[587,1588,800],{"class":603},[587,1590,1591,1593,1595,1597,1599,1602,1605,1607,1609,1612],{"class":433,"line":1269},[587,1592,739],{"class":603},[587,1594,1509],{"class":707},[587,1596,936],{"class":639},[587,1598,714],{"class":599},[587,1600,1601],{"class":603},"{panelId} ",[587,1603,1604],{"class":639},"hidden",[587,1606,714],{"class":599},[587,1608,717],{"class":603},[587,1610,1611],{"class":599},"!",[587,1613,1614],{"class":603},"open}>\n",[587,1616,1617],{"class":433,"line":1275},[587,1618,1619],{"class":603},"        {children}\n",[587,1621,1622,1624,1626],{"class":433,"line":1296},[587,1623,1584],{"class":603},[587,1625,1509],{"class":707},[587,1627,800],{"class":603},[587,1629,1630,1632,1634],{"class":433,"line":1309},[587,1631,795],{"class":603},[587,1633,1509],{"class":707},[587,1635,800],{"class":603},[587,1637,1638],{"class":433,"line":1324},[587,1639,806],{"class":603},[587,1641,1642],{"class":433,"line":1330},[587,1643,812],{"class":603},[324,1645,1646,1647,1650],{},"Both renders begin collapsed, so ",[345,1648,1649],{},"aria-expanded=\"false\""," is identical on server and client; only user interaction changes it. No warning, no stale announcement.",[387,1652,1654],{"id":1653},"complex-interactive-widgets-modals-dropdowns-tabs","Complex Interactive Widgets (Modals, Dropdowns, Tabs)",[324,1656,1657],{},"Compound components require both server data fetching and robust client interaction. When rendering overlays or dynamic menus, you must manage focus traps, keyboard navigation, and ARIA state synchronously. Portals should be used to render overlays outside the main DOM tree while maintaining logical tab order.",[324,1659,1660],{},[335,1661,548],{},[339,1663,1664,1673,1682,1689],{},[342,1665,1666,1667,397,1669,1672],{},"Trap focus within client-rendered overlays using ",[345,1668,1026],{},[345,1670,1671],{},"keydown"," listeners.",[342,1674,1675,1676,397,1678,1681],{},"Manage ",[345,1677,409],{},[345,1679,1680],{},"aria-controls"," synchronously to reflect UI state.",[342,1683,1684,1685,1688],{},"Handle ",[345,1686,1687],{},"Escape"," key dismissal and backdrop clicks without breaking focus restoration.",[342,1690,1691,1692,725],{},"For production-ready implementations, reference ",[328,1693,1695],{"href":1694},"\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",[578,1697,1699],{"className":580,"code":1698,"language":582,"meta":583,"style":583},"\u002F\u002F components\u002FAccessibleModal.tsx\n'use client';\n\nimport { useEffect, useRef, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\n\nexport function AccessibleModal({\n  isOpen,\n  onClose,\n  children,\n  titleId,\n}: {\n  isOpen: boolean;\n  onClose: () => void;\n  children: React.ReactNode;\n  titleId: string;\n}) {\n  const modalRef = useRef\u003CHTMLDivElement>(null);\n  const previousFocus = useRef\u003CHTMLElement | null>(null);\n\n  \u002F\u002F Focus trap & restoration\n  useEffect(() => {\n    if (isOpen) {\n      previousFocus.current = document.activeElement as HTMLElement;\n      modalRef.current?.focus();\n    } else {\n      previousFocus.current?.focus();\n    }\n  }, [isOpen]);\n\n  const handleKeyDown = useCallback((e: KeyboardEvent) => {\n    if (e.key === 'Escape') {\n      e.preventDefault();\n      onClose();\n    }\n    if (e.key === 'Tab') {\n      const focusable = modalRef.current?.querySelectorAll\u003CHTMLElement>(\n        'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n      );\n      if (!focusable?.length) return;\n      const first = focusable[0];\n      const last = focusable[focusable.length - 1];\n      if (e.shiftKey && document.activeElement === first) {\n        e.preventDefault();\n        last.focus();\n      } else if (!e.shiftKey && document.activeElement === last) {\n        e.preventDefault();\n        first.focus();\n      }\n    }\n  }, [onClose]);\n\n  useEffect(() => {\n    if (isOpen) document.addEventListener('keydown', handleKeyDown);\n    return () => document.removeEventListener('keydown', handleKeyDown);\n  }, [isOpen, handleKeyDown]);\n\n  if (!isOpen) return null;\n\n  return createPortal(\n    \u003Cdiv\n      ref={modalRef}\n      role=\"dialog\"\n      aria-modal=\"true\"\n      aria-labelledby={titleId}\n      tabIndex={-1}\n      style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}\n      onClick={(e) => e.target === e.currentTarget && onClose()}\n    >\n      \u003Cdiv style={{ background: '#fff', padding: '2rem', maxWidth: '500px', width: '100%' }}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>,\n    document.body\n  );\n}\n",[345,1700,1701,1706,1712,1716,1729,1743,1747,1759,1766,1773,1780,1787,1796,1807,1822,1836,1846,1851,1875,1903,1907,1912,1922,1929,1947,1957,1968,1978,1983,1989,1994,2023,2040,2051,2059,2064,2078,2101,2107,2113,2137,2156,2179,2197,2207,2217,2244,2253,2263,2269,2274,2280,2285,2296,2315,2337,2343,2348,2367,2372,2383,2391,2402,2413,2424,2435,2453,2498,2531,2537,2576,2581,2590,2600,2606,2611],{"__ignoreMap":583},[587,1702,1703],{"class":433,"line":589},[587,1704,1705],{"class":592},"\u002F\u002F components\u002FAccessibleModal.tsx\n",[587,1707,1708,1710],{"class":433,"line":596},[587,1709,378],{"class":610},[587,1711,614],{"class":603},[587,1713,1714],{"class":433,"line":617},[587,1715,621],{"emptyLinePlaceholder":620},[587,1717,1718,1720,1723,1725,1727],{"class":433,"line":624},[587,1719,600],{"class":599},[587,1721,1722],{"class":603}," { useEffect, useRef, useCallback } ",[587,1724,607],{"class":599},[587,1726,1082],{"class":610},[587,1728,614],{"class":603},[587,1730,1731,1733,1736,1738,1741],{"class":433,"line":686},[587,1732,600],{"class":599},[587,1734,1735],{"class":603}," { createPortal } ",[587,1737,607],{"class":599},[587,1739,1740],{"class":610}," 'react-dom'",[587,1742,614],{"class":603},[587,1744,1745],{"class":433,"line":692},[587,1746,621],{"emptyLinePlaceholder":620},[587,1748,1749,1751,1753,1756],{"class":433,"line":701},[587,1750,627],{"class":599},[587,1752,636],{"class":599},[587,1754,1755],{"class":639}," AccessibleModal",[587,1757,1758],{"class":603},"({\n",[587,1760,1761,1764],{"class":433,"line":736},[587,1762,1763],{"class":646},"  isOpen",[587,1765,1293],{"class":603},[587,1767,1768,1771],{"class":433,"line":745},[587,1769,1770],{"class":646},"  onClose",[587,1772,1293],{"class":603},[587,1774,1775,1778],{"class":433,"line":756},[587,1776,1777],{"class":646},"  children",[587,1779,1293],{"class":603},[587,1781,1782,1785],{"class":433,"line":767},[587,1783,1784],{"class":646},"  titleId",[587,1786,1293],{"class":603},[587,1788,1789,1792,1794],{"class":433,"line":786},[587,1790,1791],{"class":603},"}",[587,1793,653],{"class":599},[587,1795,1156],{"class":603},[587,1797,1798,1800,1802,1805],{"class":433,"line":792},[587,1799,1763],{"class":646},[587,1801,653],{"class":599},[587,1803,1804],{"class":670}," boolean",[587,1806,614],{"class":603},[587,1808,1809,1811,1813,1815,1817,1820],{"class":433,"line":803},[587,1810,1770],{"class":639},[587,1812,653],{"class":599},[587,1814,1211],{"class":603},[587,1816,1153],{"class":599},[587,1818,1819],{"class":670}," void",[587,1821,614],{"class":603},[587,1823,1824,1826,1828,1830,1832,1834],{"class":433,"line":809},[587,1825,1777],{"class":646},[587,1827,653],{"class":599},[587,1829,1438],{"class":639},[587,1831,725],{"class":603},[587,1833,1443],{"class":639},[587,1835,614],{"class":603},[587,1837,1838,1840,1842,1844],{"class":433,"line":1236},[587,1839,1784],{"class":646},[587,1841,653],{"class":599},[587,1843,671],{"class":670},[587,1845,614],{"class":603},[587,1847,1848],{"class":433,"line":1243},[587,1849,1850],{"class":603},"}) {\n",[587,1852,1853,1855,1858,1860,1863,1865,1868,1870,1873],{"class":433,"line":1269},[587,1854,1104],{"class":599},[587,1856,1857],{"class":670}," modalRef",[587,1859,1180],{"class":599},[587,1861,1862],{"class":639}," useRef",[587,1864,1126],{"class":603},[587,1866,1867],{"class":639},"HTMLDivElement",[587,1869,1132],{"class":603},[587,1871,1872],{"class":670},"null",[587,1874,1138],{"class":603},[587,1876,1877,1879,1882,1884,1886,1888,1891,1894,1897,1899,1901],{"class":433,"line":1275},[587,1878,1104],{"class":599},[587,1880,1881],{"class":670}," previousFocus",[587,1883,1180],{"class":599},[587,1885,1862],{"class":639},[587,1887,1126],{"class":603},[587,1889,1890],{"class":639},"HTMLElement",[587,1892,1893],{"class":599}," |",[587,1895,1896],{"class":670}," null",[587,1898,1132],{"class":603},[587,1900,1872],{"class":670},[587,1902,1138],{"class":603},[587,1904,1905],{"class":433,"line":1296},[587,1906,621],{"emptyLinePlaceholder":620},[587,1908,1909],{"class":433,"line":1309},[587,1910,1911],{"class":592},"  \u002F\u002F Focus trap & restoration\n",[587,1913,1914,1916,1918,1920],{"class":433,"line":1324},[587,1915,1147],{"class":639},[587,1917,1150],{"class":603},[587,1919,1153],{"class":599},[587,1921,1156],{"class":603},[587,1923,1924,1926],{"class":433,"line":1330},[587,1925,1166],{"class":599},[587,1927,1928],{"class":603}," (isOpen) {\n",[587,1930,1931,1934,1936,1939,1942,1945],{"class":433,"line":1336},[587,1932,1933],{"class":603},"      previousFocus.current ",[587,1935,714],{"class":599},[587,1937,1938],{"class":603}," document.activeElement ",[587,1940,1941],{"class":599},"as",[587,1943,1944],{"class":639}," HTMLElement",[587,1946,614],{"class":603},[587,1948,1949,1952,1955],{"class":433,"line":1342},[587,1950,1951],{"class":603},"      modalRef.current?.",[587,1953,1954],{"class":639},"focus",[587,1956,1492],{"class":603},[587,1958,1960,1963,1966],{"class":433,"line":1959},26,[587,1961,1962],{"class":603},"    } ",[587,1964,1965],{"class":599},"else",[587,1967,1156],{"class":603},[587,1969,1971,1974,1976],{"class":433,"line":1970},27,[587,1972,1973],{"class":603},"      previousFocus.current?.",[587,1975,1954],{"class":639},[587,1977,1492],{"class":603},[587,1979,1981],{"class":433,"line":1980},28,[587,1982,1224],{"class":603},[587,1984,1986],{"class":433,"line":1985},29,[587,1987,1988],{"class":603},"  }, [isOpen]);\n",[587,1990,1992],{"class":433,"line":1991},30,[587,1993,621],{"emptyLinePlaceholder":620},[587,1995,1997,1999,2002,2004,2007,2009,2012,2014,2017,2019,2021],{"class":433,"line":1996},31,[587,1998,1104],{"class":599},[587,2000,2001],{"class":670}," handleKeyDown",[587,2003,1180],{"class":599},[587,2005,2006],{"class":639}," useCallback",[587,2008,1556],{"class":603},[587,2010,2011],{"class":646},"e",[587,2013,653],{"class":599},[587,2015,2016],{"class":639}," KeyboardEvent",[587,2018,1259],{"class":603},[587,2020,1153],{"class":599},[587,2022,1156],{"class":603},[587,2024,2026,2028,2031,2034,2037],{"class":433,"line":2025},32,[587,2027,1166],{"class":599},[587,2029,2030],{"class":603}," (e.key ",[587,2032,2033],{"class":599},"===",[587,2035,2036],{"class":610}," 'Escape'",[587,2038,2039],{"class":603},") {\n",[587,2041,2043,2046,2049],{"class":433,"line":2042},33,[587,2044,2045],{"class":603},"      e.",[587,2047,2048],{"class":639},"preventDefault",[587,2050,1492],{"class":603},[587,2052,2054,2057],{"class":433,"line":2053},34,[587,2055,2056],{"class":639},"      onClose",[587,2058,1492],{"class":603},[587,2060,2062],{"class":433,"line":2061},35,[587,2063,1224],{"class":603},[587,2065,2067,2069,2071,2073,2076],{"class":433,"line":2066},36,[587,2068,1166],{"class":599},[587,2070,2030],{"class":603},[587,2072,2033],{"class":599},[587,2074,2075],{"class":610}," 'Tab'",[587,2077,2039],{"class":603},[587,2079,2081,2083,2086,2088,2091,2094,2096,2098],{"class":433,"line":2080},37,[587,2082,1174],{"class":599},[587,2084,2085],{"class":670}," focusable",[587,2087,1180],{"class":599},[587,2089,2090],{"class":603}," modalRef.current?.",[587,2092,2093],{"class":639},"querySelectorAll",[587,2095,1126],{"class":603},[587,2097,1890],{"class":639},[587,2099,2100],{"class":603},">(\n",[587,2102,2104],{"class":433,"line":2103},38,[587,2105,2106],{"class":610},"        'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n",[587,2108,2110],{"class":433,"line":2109},39,[587,2111,2112],{"class":603},"      );\n",[587,2114,2116,2119,2122,2124,2127,2130,2132,2135],{"class":433,"line":2115},40,[587,2117,2118],{"class":599},"      if",[587,2120,2121],{"class":603}," (",[587,2123,1611],{"class":599},[587,2125,2126],{"class":603},"focusable?.",[587,2128,2129],{"class":670},"length",[587,2131,1259],{"class":603},[587,2133,2134],{"class":599},"return",[587,2136,614],{"class":603},[587,2138,2140,2142,2145,2147,2150,2153],{"class":433,"line":2139},41,[587,2141,1174],{"class":599},[587,2143,2144],{"class":670}," first",[587,2146,1180],{"class":599},[587,2148,2149],{"class":603}," focusable[",[587,2151,2152],{"class":670},"0",[587,2154,2155],{"class":603},"];\n",[587,2157,2159,2161,2164,2166,2169,2171,2174,2177],{"class":433,"line":2158},42,[587,2160,1174],{"class":599},[587,2162,2163],{"class":670}," last",[587,2165,1180],{"class":599},[587,2167,2168],{"class":603}," focusable[focusable.",[587,2170,2129],{"class":670},[587,2172,2173],{"class":599}," -",[587,2175,2176],{"class":670}," 1",[587,2178,2155],{"class":603},[587,2180,2182,2184,2187,2190,2192,2194],{"class":433,"line":2181},43,[587,2183,2118],{"class":599},[587,2185,2186],{"class":603}," (e.shiftKey ",[587,2188,2189],{"class":599},"&&",[587,2191,1938],{"class":603},[587,2193,2033],{"class":599},[587,2195,2196],{"class":603}," first) {\n",[587,2198,2200,2203,2205],{"class":433,"line":2199},44,[587,2201,2202],{"class":603},"        e.",[587,2204,2048],{"class":639},[587,2206,1492],{"class":603},[587,2208,2210,2213,2215],{"class":433,"line":2209},45,[587,2211,2212],{"class":603},"        last.",[587,2214,1954],{"class":639},[587,2216,1492],{"class":603},[587,2218,2220,2223,2225,2228,2230,2232,2235,2237,2239,2241],{"class":433,"line":2219},46,[587,2221,2222],{"class":603},"      } ",[587,2224,1965],{"class":599},[587,2226,2227],{"class":599}," if",[587,2229,2121],{"class":603},[587,2231,1611],{"class":599},[587,2233,2234],{"class":603},"e.shiftKey ",[587,2236,2189],{"class":599},[587,2238,1938],{"class":603},[587,2240,2033],{"class":599},[587,2242,2243],{"class":603}," last) {\n",[587,2245,2247,2249,2251],{"class":433,"line":2246},47,[587,2248,2202],{"class":603},[587,2250,2048],{"class":639},[587,2252,1492],{"class":603},[587,2254,2256,2259,2261],{"class":433,"line":2255},48,[587,2257,2258],{"class":603},"        first.",[587,2260,1954],{"class":639},[587,2262,1492],{"class":603},[587,2264,2266],{"class":433,"line":2265},49,[587,2267,2268],{"class":603},"      }\n",[587,2270,2272],{"class":433,"line":2271},50,[587,2273,1224],{"class":603},[587,2275,2277],{"class":433,"line":2276},51,[587,2278,2279],{"class":603},"  }, [onClose]);\n",[587,2281,2283],{"class":433,"line":2282},52,[587,2284,621],{"emptyLinePlaceholder":620},[587,2286,2288,2290,2292,2294],{"class":433,"line":2287},53,[587,2289,1147],{"class":639},[587,2291,1150],{"class":603},[587,2293,1153],{"class":599},[587,2295,1156],{"class":603},[587,2297,2299,2301,2304,2307,2309,2312],{"class":433,"line":2298},54,[587,2300,1166],{"class":599},[587,2302,2303],{"class":603}," (isOpen) document.",[587,2305,2306],{"class":639},"addEventListener",[587,2308,1193],{"class":603},[587,2310,2311],{"class":610},"'keydown'",[587,2313,2314],{"class":603},", handleKeyDown);\n",[587,2316,2318,2321,2323,2325,2328,2331,2333,2335],{"class":433,"line":2317},55,[587,2319,2320],{"class":599},"    return",[587,2322,1211],{"class":603},[587,2324,1153],{"class":599},[587,2326,2327],{"class":603}," document.",[587,2329,2330],{"class":639},"removeEventListener",[587,2332,1193],{"class":603},[587,2334,2311],{"class":610},[587,2336,2314],{"class":603},[587,2338,2340],{"class":433,"line":2339},56,[587,2341,2342],{"class":603},"  }, [isOpen, handleKeyDown]);\n",[587,2344,2346],{"class":433,"line":2345},57,[587,2347,621],{"emptyLinePlaceholder":620},[587,2349,2351,2354,2356,2358,2361,2363,2365],{"class":433,"line":2350},58,[587,2352,2353],{"class":599},"  if",[587,2355,2121],{"class":603},[587,2357,1611],{"class":599},[587,2359,2360],{"class":603},"isOpen) ",[587,2362,2134],{"class":599},[587,2364,1896],{"class":670},[587,2366,614],{"class":603},[587,2368,2370],{"class":433,"line":2369},59,[587,2371,621],{"emptyLinePlaceholder":620},[587,2373,2375,2377,2380],{"class":433,"line":2374},60,[587,2376,695],{"class":599},[587,2378,2379],{"class":639}," createPortal",[587,2381,2382],{"class":603},"(\n",[587,2384,2386,2388],{"class":433,"line":2385},61,[587,2387,704],{"class":603},[587,2389,2390],{"class":707},"div\n",[587,2392,2394,2397,2399],{"class":433,"line":2393},62,[587,2395,2396],{"class":639},"      ref",[587,2398,714],{"class":599},[587,2400,2401],{"class":603},"{modalRef}\n",[587,2403,2405,2408,2410],{"class":433,"line":2404},63,[587,2406,2407],{"class":639},"      role",[587,2409,714],{"class":599},[587,2411,2412],{"class":610},"\"dialog\"\n",[587,2414,2416,2419,2421],{"class":433,"line":2415},64,[587,2417,2418],{"class":639},"      aria-modal",[587,2420,714],{"class":599},[587,2422,2423],{"class":610},"\"true\"\n",[587,2425,2427,2430,2432],{"class":433,"line":2426},65,[587,2428,2429],{"class":639},"      aria-labelledby",[587,2431,714],{"class":599},[587,2433,2434],{"class":603},"{titleId}\n",[587,2436,2438,2441,2443,2445,2448,2451],{"class":433,"line":2437},66,[587,2439,2440],{"class":639},"      tabIndex",[587,2442,714],{"class":599},[587,2444,717],{"class":603},[587,2446,2447],{"class":599},"-",[587,2449,2450],{"class":670},"1",[587,2452,812],{"class":603},[587,2454,2456,2459,2461,2464,2467,2470,2472,2475,2478,2481,2484,2487,2490,2493,2495],{"class":433,"line":2455},67,[587,2457,2458],{"class":639},"      style",[587,2460,714],{"class":599},[587,2462,2463],{"class":603},"{{ position: ",[587,2465,2466],{"class":610},"'fixed'",[587,2468,2469],{"class":603},", inset: ",[587,2471,2152],{"class":670},[587,2473,2474],{"class":603},", background: ",[587,2476,2477],{"class":610},"'rgba(0,0,0,0.5)'",[587,2479,2480],{"class":603},", display: ",[587,2482,2483],{"class":610},"'flex'",[587,2485,2486],{"class":603},", alignItems: ",[587,2488,2489],{"class":610},"'center'",[587,2491,2492],{"class":603},", justifyContent: ",[587,2494,2489],{"class":610},[587,2496,2497],{"class":603}," }}\n",[587,2499,2501,2504,2506,2509,2511,2513,2515,2518,2520,2523,2525,2528],{"class":433,"line":2500},68,[587,2502,2503],{"class":639},"      onClick",[587,2505,714],{"class":599},[587,2507,2508],{"class":603},"{(",[587,2510,2011],{"class":646},[587,2512,1259],{"class":603},[587,2514,1153],{"class":599},[587,2516,2517],{"class":603}," e.target ",[587,2519,2033],{"class":599},[587,2521,2522],{"class":603}," e.currentTarget ",[587,2524,2189],{"class":599},[587,2526,2527],{"class":639}," onClose",[587,2529,2530],{"class":603},"()}\n",[587,2532,2534],{"class":433,"line":2533},69,[587,2535,2536],{"class":603},"    >\n",[587,2538,2540,2542,2544,2547,2549,2552,2555,2558,2561,2564,2567,2570,2573],{"class":433,"line":2539},70,[587,2541,739],{"class":603},[587,2543,1509],{"class":707},[587,2545,2546],{"class":639}," style",[587,2548,714],{"class":599},[587,2550,2551],{"class":603},"{{ background: ",[587,2553,2554],{"class":610},"'#fff'",[587,2556,2557],{"class":603},", padding: ",[587,2559,2560],{"class":610},"'2rem'",[587,2562,2563],{"class":603},", maxWidth: ",[587,2565,2566],{"class":610},"'500px'",[587,2568,2569],{"class":603},", width: ",[587,2571,2572],{"class":610},"'100%'",[587,2574,2575],{"class":603}," }}>\n",[587,2577,2579],{"class":433,"line":2578},71,[587,2580,1619],{"class":603},[587,2582,2584,2586,2588],{"class":433,"line":2583},72,[587,2585,1584],{"class":603},[587,2587,1509],{"class":707},[587,2589,800],{"class":603},[587,2591,2593,2595,2597],{"class":433,"line":2592},73,[587,2594,795],{"class":603},[587,2596,1509],{"class":707},[587,2598,2599],{"class":603},">,\n",[587,2601,2603],{"class":433,"line":2602},74,[587,2604,2605],{"class":603},"    document.body\n",[587,2607,2609],{"class":433,"line":2608},75,[587,2610,806],{"class":603},[587,2612,2614],{"class":433,"line":2613},76,[587,2615,812],{"class":603},[324,2617,2618,2620],{},[335,2619,817],{}," Validate focus trap behavior, keyboard navigation order, and screen reader role announcements using automated axe scans and manual keyboard-only navigation.",[820,2622,2624],{"id":2623},"hydrating-server-streamed-lists-without-losing-focus","Hydrating Server-Streamed Lists Without Losing Focus",[324,2626,2627,2628,2631],{},"When a Server Component streams a list and a client widget filters or paginates it, focus can land on a node that React replaces during the next stream chunk. Anchor focus to a stable landmark (a results heading with ",[345,2629,2630],{},"tabIndex={-1}",") rather than to an individual row, and move focus there after each update so keyboard users are never stranded on a detached element.",[578,2633,2635],{"className":580,"code":2634,"language":582,"meta":583,"style":583},"'use client';\nimport { useEffect, useRef } from 'react';\n\nexport function ResultsRegion({ count, children }: { count: number; children: React.ReactNode }) {\n  const headingRef = useRef\u003CHTMLHeadingElement>(null);\n\n  useEffect(() => {\n    \u002F\u002F After a filter\u002Fpagination update, return focus to the stable region heading.\n    headingRef.current?.focus();\n  }, [count]);\n\n  return (\n    \u003Csection aria-labelledby=\"results-heading\">\n      \u003Ch2 id=\"results-heading\" ref={headingRef} tabIndex={-1}>\n        {count} results\n      \u003C\u002Fh2>\n      \u003Cul>{children}\u003C\u002Ful>\n    \u003C\u002Fsection>\n  );\n}\n",[345,2636,2637,2643,2656,2660,2705,2727,2731,2741,2746,2755,2760,2764,2770,2786,2819,2824,2832,2845,2853,2857],{"__ignoreMap":583},[587,2638,2639,2641],{"class":433,"line":589},[587,2640,378],{"class":610},[587,2642,614],{"class":603},[587,2644,2645,2647,2650,2652,2654],{"class":433,"line":596},[587,2646,600],{"class":599},[587,2648,2649],{"class":603}," { useEffect, useRef } ",[587,2651,607],{"class":599},[587,2653,1082],{"class":610},[587,2655,614],{"class":603},[587,2657,2658],{"class":433,"line":617},[587,2659,621],{"emptyLinePlaceholder":620},[587,2661,2662,2664,2666,2669,2671,2674,2676,2678,2680,2682,2684,2686,2688,2691,2693,2695,2697,2699,2701,2703],{"class":433,"line":624},[587,2663,627],{"class":599},[587,2665,636],{"class":599},[587,2667,2668],{"class":639}," ResultsRegion",[587,2670,643],{"class":603},[587,2672,2673],{"class":646},"count",[587,2675,569],{"class":603},[587,2677,1417],{"class":646},[587,2679,650],{"class":603},[587,2681,653],{"class":599},[587,2683,656],{"class":603},[587,2685,2673],{"class":646},[587,2687,653],{"class":599},[587,2689,2690],{"class":670}," number",[587,2692,674],{"class":603},[587,2694,1417],{"class":646},[587,2696,653],{"class":599},[587,2698,1438],{"class":639},[587,2700,725],{"class":603},[587,2702,1443],{"class":639},[587,2704,1446],{"class":603},[587,2706,2707,2709,2712,2714,2716,2718,2721,2723,2725],{"class":433,"line":686},[587,2708,1104],{"class":599},[587,2710,2711],{"class":670}," headingRef",[587,2713,1180],{"class":599},[587,2715,1862],{"class":639},[587,2717,1126],{"class":603},[587,2719,2720],{"class":639},"HTMLHeadingElement",[587,2722,1132],{"class":603},[587,2724,1872],{"class":670},[587,2726,1138],{"class":603},[587,2728,2729],{"class":433,"line":692},[587,2730,621],{"emptyLinePlaceholder":620},[587,2732,2733,2735,2737,2739],{"class":433,"line":701},[587,2734,1147],{"class":639},[587,2736,1150],{"class":603},[587,2738,1153],{"class":599},[587,2740,1156],{"class":603},[587,2742,2743],{"class":433,"line":736},[587,2744,2745],{"class":592},"    \u002F\u002F After a filter\u002Fpagination update, return focus to the stable region heading.\n",[587,2747,2748,2751,2753],{"class":433,"line":745},[587,2749,2750],{"class":603},"    headingRef.current?.",[587,2752,1954],{"class":639},[587,2754,1492],{"class":603},[587,2756,2757],{"class":433,"line":756},[587,2758,2759],{"class":603},"  }, [count]);\n",[587,2761,2762],{"class":433,"line":767},[587,2763,621],{"emptyLinePlaceholder":620},[587,2765,2766,2768],{"class":433,"line":786},[587,2767,695],{"class":599},[587,2769,698],{"class":603},[587,2771,2772,2774,2777,2779,2781,2784],{"class":433,"line":792},[587,2773,704],{"class":603},[587,2775,2776],{"class":707},"section",[587,2778,711],{"class":639},[587,2780,714],{"class":599},[587,2782,2783],{"class":610},"\"results-heading\"",[587,2785,800],{"class":603},[587,2787,2788,2790,2792,2794,2796,2798,2801,2803,2806,2809,2811,2813,2815,2817],{"class":433,"line":803},[587,2789,739],{"class":603},[587,2791,387],{"class":707},[587,2793,936],{"class":639},[587,2795,714],{"class":599},[587,2797,2783],{"class":610},[587,2799,2800],{"class":639}," ref",[587,2802,714],{"class":599},[587,2804,2805],{"class":603},"{headingRef} ",[587,2807,2808],{"class":639},"tabIndex",[587,2810,714],{"class":599},[587,2812,717],{"class":603},[587,2814,2447],{"class":599},[587,2816,2450],{"class":670},[587,2818,733],{"class":603},[587,2820,2821],{"class":433,"line":809},[587,2822,2823],{"class":603},"        {count} results\n",[587,2825,2826,2828,2830],{"class":433,"line":1236},[587,2827,1584],{"class":603},[587,2829,387],{"class":707},[587,2831,800],{"class":603},[587,2833,2834,2836,2838,2841,2843],{"class":433,"line":1243},[587,2835,739],{"class":603},[587,2837,339],{"class":707},[587,2839,2840],{"class":603},">{children}\u003C\u002F",[587,2842,339],{"class":707},[587,2844,800],{"class":603},[587,2846,2847,2849,2851],{"class":433,"line":1269},[587,2848,795],{"class":603},[587,2850,2776],{"class":707},[587,2852,800],{"class":603},[587,2854,2855],{"class":433,"line":1275},[587,2856,806],{"class":603},[587,2858,2859],{"class":433,"line":1296},[587,2860,812],{"class":603},[387,2862,2864],{"id":2863},"common-pitfalls","Common Pitfalls",[339,2866,2867,2873,2882,2892,2905],{},[342,2868,2869,2872],{},[335,2870,2871],{},"Hydration Mismatch Caused by Client-Only ARIA Attributes:"," Applying ARIA states that differ between SSR and client mount will trigger React hydration errors. Always initialize with matching defaults.",[342,2874,2875,2881],{},[335,2876,2877,2878,2880],{},"Overusing ",[345,2879,378],{}," at the Page Root:"," Placing the directive at the layout or page level forces the entire subtree into client-side rendering, negating RSC performance and accessibility benefits.",[342,2883,2884,2887,2888,2891],{},[335,2885,2886],{},"Failing to Restore Focus After Async Data Loads:"," When server components stream in content, focus can be lost. Implement explicit ",[345,2889,2890],{},"focus()"," calls once data resolves.",[342,2893,2894,2897,2898,397,2901,2904],{},[335,2895,2896],{},"Ignoring Keyboard Navigation for Dynamically Injected Content:"," Portals and lazy-loaded widgets must be explicitly added to the DOM's tab order and tested with ",[345,2899,2900],{},"Tab",[345,2902,2903],{},"Shift+Tab"," sequences.",[342,2906,2907,2912],{},[335,2908,2909,2910],{},"Silencing hydration warnings with ",[345,2911,1359],{}," instead of rendering a server-safe resting state, hiding a genuine ARIA divergence.",[387,2914,2916],{"id":2915},"how-to-verify","How to Verify",[339,2918,2919,2929],{},[342,2920,2921,2924,2925,2928],{},[335,2922,2923],{},"Automated:"," Run ",[345,2926,2927],{},"jest-axe"," against the server-rendered output (before hydration) and against the mounted client tree to confirm both pass. Add a Next.js build check or a unit assertion that no component logs a hydration mismatch for ARIA attributes—divergences surface as console errors you can fail the test on.",[342,2930,2931,2934,2935,2937],{},[335,2932,2933],{},"Manual:"," Disable JavaScript in DevTools and confirm landmarks, headings, links, and forms remain operable. Re-enable JavaScript and, using NVDA or VoiceOver plus keyboard only, open a modal\u002Fdropdown to confirm focus is trapped, ",[345,2936,409],{}," updates are announced, and focus returns to the trigger on close. Inspect the accessibility tree in DevTools to confirm dynamic ARIA matches the visible UI state.",[387,2939,2941],{"id":2940},"frequently-asked-questions","Frequently Asked Questions",[324,2943,2944,2947,2948,2950],{},[335,2945,2946],{},"Can React Server Components handle interactive accessibility features directly?","\nNo. Server Components cannot use hooks, event listeners, or browser APIs. Interactive accessibility features like focus management, keyboard traps, and dynamic ARIA updates must be delegated to Client Components using the ",[345,2949,378],{}," directive.",[324,2952,2953,2956,2957,2959],{},[335,2954,2955],{},"How do I prevent hydration mismatches when adding ARIA attributes?","\nEnsure that any ARIA attributes dependent on client state are initialized with server-safe defaults. Use ",[345,2958,1026],{}," to apply dynamic attributes only after the component mounts, or pass serialized state from the server to maintain consistency during hydration.",[324,2961,2962,2965,2966,2968],{},[335,2963,2964],{},"What is the best way to manage focus when navigating between server-rendered routes?","\nImplement a client-side route change listener that programmatically moves focus to a designated landmark or heading. Combine this with ",[345,2967,1033],{}," announcements to inform screen reader users of the new page context without relying on full page reloads.",[324,2970,2971,2974],{},[335,2972,2973],{},"Should semantic HTML and ARIA come from the server or the client?","\nStatic semantics—landmarks, headings, labels, roles, and resting ARIA values—should render on the server so assistive technology has a complete document in the first response. Only state that changes with user interaction belongs on the client, hydrated from a server-safe default.",[324,2976,2977,2983,2984,2987],{},[335,2978,2979,2980,2982],{},"Does ",[345,2981,378],{}," make a component inaccessible to screen readers before hydration?","\nNo. A client component still renders HTML on the server, so its markup and static ARIA are present immediately. What is unavailable before hydration is ",[403,2985,2986],{},"behavior","—event handlers and focus management—which is why critical interactions should degrade gracefully or rely on native elements.",[387,2989,2991],{"id":2990},"related-guides","Related guides",[339,2993,2994,2998,3002,3006],{},[342,2995,2996],{},[328,2997,94],{"href":330},[342,2999,3000],{},[328,3001,1695],{"href":1694},[342,3003,3004],{},[328,3005,542],{"href":541},[342,3007,3008],{},[328,3009,181],{"href":1053},[3011,3012,3013],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .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":583,"searchDepth":596,"depth":596,"links":3015},[3016,3017,3020,3024,3027,3028,3029,3030],{"id":389,"depth":596,"text":390},{"id":534,"depth":596,"text":535,"children":3018},[3019],{"id":822,"depth":617,"text":823},{"id":1010,"depth":596,"text":1011,"children":3021},[3022],{"id":1355,"depth":617,"text":3023},"Avoiding the suppressHydrationWarning Trap",{"id":1653,"depth":596,"text":1654,"children":3025},[3026],{"id":2623,"depth":617,"text":2624},{"id":2863,"depth":596,"text":2864},{"id":2915,"depth":596,"text":2916},{"id":2940,"depth":596,"text":2941},{"id":2990,"depth":596,"text":2991},null,"Balance Server Components and client interactivity with accessibility-first patterns for focus, announcements, and progressive enhancement.","md",{},false,{"title":205,"description":3032},"PixxT1gSFucZEkTC89vZ5EP4_3LlW5CGjud0B9gsZU8",[3039,3078,3079,3142],{"title":5,"path":6,"stem":7,"children":3040,"page":-1},[3041,3042,3045,3048,3054,3060,3069,3075],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":3043},[3044],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":3046},[3047],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":3049,"page":-1},[3050,3051],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":3052},[3053],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":3055,"page":-1},[3056,3057],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":3058},[3059],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":3061,"page":-1},[3062,3063,3066],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":3064},[3065],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":3067},[3068],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":3070},[3071,3072],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":3073},[3074],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":3076},[3077],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":3080,"page":-1},[3081,3082,3088,3100,3112,3115,3124,3136],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":3083,"page":-1},[3084,3085],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":3086},[3087],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":3089,"page":-1},[3090,3091,3094,3097],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":3092},[3093],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":3095},[3096],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":3098},[3099],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":3101,"page":-1},[3102,3103,3106,3109],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":3104},[3105],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":3107},[3108],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":3110},[3111],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":3113},[3114],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":3116,"page":-1},[3117,3118,3121],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":3119},[3120],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":3122},[3123],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":3125,"page":-1},[3126,3127,3130,3133],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":3128},[3129],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":3131},[3132],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":3134},[3135],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":3137,"page":-1},[3138,3139],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":3140},[3141],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":3143,"page":-1},[3144,3145,3154,3163,3172,3181],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":3146},[3147,3148,3151],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":3149},[3150],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":3152},[3153],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":3155,"page":-1},[3156,3157,3160],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":3158},[3159],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":3161},[3162],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":3164,"page":-1},[3165,3166,3169],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":3167},[3168],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":3170},[3171],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":3173,"page":-1},[3174,3175,3178],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":3176},[3177],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":3179},[3180],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":3182,"page":-1},[3183,3184,3187],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":3185},[3186],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":3188},[3189],{"title":309,"path":310,"stem":311},1781785523211]