[{"data":1,"prerenderedAt":2804},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions\u002F":314,"content-navigation":2652},[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":187,"body":316,"date":2645,"description":2646,"extension":2647,"image":2645,"meta":2648,"modifiedAt":2645,"navigation":736,"noindex":2649,"path":188,"publishedAt":2645,"seo":2650,"stem":189,"updatedAt":2645,"__hash__":2651},"content\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fbuilding-a-useannouncer-hook-for-live-regions\u002Findex.md",{"type":317,"value":318,"toc":2634},"minimark",[319,323,353,356,361,409,575,577,581,584,605,608,704,706,710,717,1794,1816,1818,1822,1842,1987,1990,2156,2158,2162,2168,2473,2479,2485,2493,2495,2499,2544,2546,2550,2558,2560,2564,2569,2572,2577,2583,2588,2591,2596,2604,2606,2610,2630],[320,321,187],"h1",{"id":322},"building-a-useannouncer-hook-for-live-regions",[324,325,326,327,331,332,335,336,339,340,344,345,348,349,352],"p",{},"Scatter ",[328,329,330],"code",{},"aria-live"," regions across a codebase and you inherit a class of bugs that never show up in a visual diff: identical messages that fail to re-announce, rapid updates that clobber each other, regions mounted too late to register, and duplicate regions that make screen readers stutter. The fix is to centralize. One ",[328,333,334],{},"useAnnouncer"," hook, backed by a single provider that mounts exactly one polite region and one assertive region at the app root, gives every component a clean ",[328,337,338],{},"announce(message, { assertive })"," call and makes correct behavior the default. This guide builds that hook end to end, covering message clearing, queueing, and SSR safety. It belongs to the ",[341,342,181],"a",{"href":343},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F"," and the wider ",[341,346,94],{"href":347},"\u002Freact-nextjs-accessibility-patterns\u002F"," set, and it implements WCAG ",[328,350,351],{},"4.1.3 Status Messages",".",[354,355],"hr",{},[357,358,360],"h2",{"id":359},"prerequisites","Prerequisites",[362,363,364,372,383,398],"ul",{},[365,366,367,371],"li",{},[368,369,370],"strong",{},"React 18+",". The queue uses functional state updates and effects; SSR safety assumes the App Router or any framework that renders on the server.",[365,373,374,375,378,379,382],{},"A single ",[368,376,377],{},"provider mounted at the root"," (Next.js ",[328,380,381],{},"app\u002Flayout.tsx",", or the top of your component tree). The whole point is that the live regions exist once and from first paint.",[365,384,385,386,389,390,393,394,397],{},"A ",[328,387,388],{},".sr-only"," utility class that hides content visually while keeping it in the accessibility tree (use the ",[328,391,392],{},"clip"," technique, never ",[328,395,396],{},"display:none",", which removes it from the tree).",[365,399,400,401,404,405,408],{},"Understanding that two regions are needed: ",[328,402,403],{},"aria-live=\"polite\""," for routine messages and ",[328,406,407],{},"aria-live=\"assertive\""," for urgent ones. A single region cannot be both.",[410,411,416],"pre",{"className":412,"code":413,"language":414,"meta":415,"style":415},"language-css shiki shiki-themes github-light github-dark",".sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  border: 0;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n}\n","css","",[328,417,418,430,446,463,477,492,505,517,530,556,569],{"__ignoreMap":415},[419,420,423,426],"span",{"class":421,"line":422},"line",1,[419,424,388],{"class":425},"sScJk",[419,427,429],{"class":428},"sVt8B"," {\n",[419,431,433,437,440,443],{"class":421,"line":432},2,[419,434,436],{"class":435},"sj4cs","  position",[419,438,439],{"class":428},": ",[419,441,442],{"class":435},"absolute",[419,444,445],{"class":428},";\n",[419,447,449,452,454,457,461],{"class":421,"line":448},3,[419,450,451],{"class":435},"  width",[419,453,439],{"class":428},[419,455,456],{"class":435},"1",[419,458,460],{"class":459},"szBVR","px",[419,462,445],{"class":428},[419,464,466,469,471,473,475],{"class":421,"line":465},4,[419,467,468],{"class":435},"  height",[419,470,439],{"class":428},[419,472,456],{"class":435},[419,474,460],{"class":459},[419,476,445],{"class":428},[419,478,480,483,485,488,490],{"class":421,"line":479},5,[419,481,482],{"class":435},"  margin",[419,484,439],{"class":428},[419,486,487],{"class":435},"-1",[419,489,460],{"class":459},[419,491,445],{"class":428},[419,493,495,498,500,503],{"class":421,"line":494},6,[419,496,497],{"class":435},"  padding",[419,499,439],{"class":428},[419,501,502],{"class":435},"0",[419,504,445],{"class":428},[419,506,508,511,513,515],{"class":421,"line":507},7,[419,509,510],{"class":435},"  border",[419,512,439],{"class":428},[419,514,502],{"class":435},[419,516,445],{"class":428},[419,518,520,523,525,528],{"class":421,"line":519},8,[419,521,522],{"class":435},"  overflow",[419,524,439],{"class":428},[419,526,527],{"class":435},"hidden",[419,529,445],{"class":428},[419,531,533,536,538,541,544,546,549,551,553],{"class":421,"line":532},9,[419,534,535],{"class":435},"  clip",[419,537,439],{"class":428},[419,539,540],{"class":435},"rect",[419,542,543],{"class":428},"(",[419,545,502],{"class":435},[419,547,548],{"class":435}," 0",[419,550,548],{"class":435},[419,552,548],{"class":435},[419,554,555],{"class":428},");\n",[419,557,559,562,564,567],{"class":421,"line":558},10,[419,560,561],{"class":435},"  white-space",[419,563,439],{"class":428},[419,565,566],{"class":435},"nowrap",[419,568,445],{"class":428},[419,570,572],{"class":421,"line":571},11,[419,573,574],{"class":428},"}\n",[354,576],{},[357,578,580],{"id":579},"why-a-single-hook-beats-ad-hoc-regions","Why a Single Hook Beats Ad-Hoc Regions",[324,582,583],{},"Three recurring failures motivate the design:",[585,586,587,593,599],"ol",{},[365,588,589,592],{},[368,590,591],{},"Duplicate-message silence."," Setting a live region's text to the same string twice in a row produces no DOM mutation, so the screen reader stays silent. A user who triggers \"Item added to cart\" twice hears it once. The hook must force a change.",[365,594,595,598],{},[368,596,597],{},"Clobbering."," Two announcements fired within the same render cycle overwrite each other; the screen reader only ever speaks the last one. The hook must queue.",[365,600,601,604],{},[368,602,603],{},"Late mounting."," A region created at the moment of the first announcement is often missed by assistive technology, which registers live regions when they enter the tree. The hook's provider must mount the regions up front and leave them empty.",[324,606,607],{},"A centralized provider plus a thin hook solves all three in one place, so individual components never reason about live-region mechanics.",[609,610,617,618,617,622,617,626,617,636,617,644,617,646,617,649,617,651,617,655,617,661,617,664,617,667,617,674,617,678,617,684,617,688,617,692,617,697,617,700],"svg",{"role":611,"ariaLabelledBy":612,"viewBox":615,"style":616},"img",[613,614],"annT","annD","0 0 760 200","width:100%;height:auto;max-width:760px","\n  ",[619,620,621],"title",{"id":613},"useAnnouncer queue and region routing",[623,624,625],"desc",{"id":614},"Multiple announce calls enter a FIFO queue; the provider drains one message at a time into either the single polite region or the single assertive region, clearing between messages so identical strings re-announce.",[540,627],{"style":628,"x":629,"y":630,"width":631,"height":632,"rx":633,"fill":634,"stroke":635},"stroke-width:2","10","20","150","30","5","var(--primary-soft)","currentColor",[637,638,643],"text",{"style":639,"x":640,"y":641,"fill":642},"text-anchor:middle","85","40","var(--text)","announce(\"Saved\")",[540,645],{"style":628,"x":629,"y":640,"width":631,"height":632,"rx":633,"fill":634,"stroke":635},[637,647,643],{"style":639,"x":640,"y":648,"fill":642},"105",[540,650],{"style":628,"x":629,"y":631,"width":631,"height":632,"rx":633,"fill":634,"stroke":635},[637,652,654],{"style":639,"x":640,"y":653,"fill":642},"170","announce(\"Error\", a)",[421,656],{"style":628,"x1":657,"y1":658,"x2":659,"y2":660,"stroke":635},"160","35","220","95",[421,662],{"style":628,"x1":657,"y1":663,"x2":659,"y2":663,"stroke":635},"100",[421,665],{"style":628,"x1":657,"y1":666,"x2":659,"y2":648,"stroke":635},"165",[540,668],{"style":628,"x":659,"y":669,"width":670,"height":671,"rx":672,"fill":673,"stroke":635},"78","140","44","6","var(--surface)",[637,675,677],{"style":639,"x":676,"y":648,"fill":642},"290","FIFO queue",[421,679],{"style":628,"x1":680,"y1":681,"x2":682,"y2":683,"stroke":635},"360","90","470","55",[421,685],{"style":628,"x1":680,"y1":686,"x2":682,"y2":687,"stroke":635},"110","145",[540,689],{"style":628,"x":682,"y":690,"width":691,"height":671,"rx":672,"fill":673,"stroke":635},"32","270",[637,693,696],{"style":639,"x":694,"y":695,"fill":642},"605","59","Polite region (aria-live=\"polite\")",[540,698],{"style":628,"x":682,"y":699,"width":691,"height":671,"rx":672,"fill":673,"stroke":635},"124",[637,701,703],{"style":639,"x":694,"y":702,"fill":642},"151","Assertive region (assertive)",[354,705],{},[357,707,709],{"id":708},"the-provider-and-the-hook","The Provider and the Hook",[324,711,712,713,716],{},"The provider holds two queues, drains them one message at a time, and clears each region before writing the next message so identical consecutive strings still register as a mutation. It exposes ",[328,714,715],{},"announce"," through context.",[410,718,722],{"className":719,"code":720,"language":721,"meta":415,"style":415},"language-tsx shiki shiki-themes github-light github-dark","'use client';\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from 'react';\n\ninterface AnnounceOptions {\n  assertive?: boolean;\n}\n\ninterface AnnouncerApi {\n  announce: (message: string, options?: AnnounceOptions) => void;\n}\n\nconst AnnouncerContext = createContext\u003CAnnouncerApi | null>(null);\n\nexport function AnnouncerProvider({ children }: { children: ReactNode }) {\n  const [polite, setPolite] = useState('');\n  const [assertive, setAssertive] = useState('');\n\n  \u002F\u002F Refs hold the pending queues so enqueueing never depends on render timing.\n  const politeQueue = useRef\u003Cstring[]>([]);\n  const assertiveQueue = useRef\u003Cstring[]>([]);\n  const draining = useRef({ polite: false, assertive: false });\n\n  const drain = useCallback((channel: 'polite' | 'assertive') => {\n    if (draining.current[channel]) return;\n    const queue = channel === 'polite' ? politeQueue : assertiveQueue;\n    const setText = channel === 'polite' ? setPolite : setAssertive;\n    const next = queue.current.shift();\n    if (next === undefined) return;\n\n    draining.current[channel] = true;\n    \u002F\u002F Clear first so an identical next message is still a DOM change (4.1.3).\n    setText('');\n    requestAnimationFrame(() => {\n      setText(next);\n      \u002F\u002F Give AT time to read before the next item; tune to taste.\n      setTimeout(() => {\n        draining.current[channel] = false;\n        drain(channel); \u002F\u002F Pull the next queued message, if any.\n      }, 150);\n    });\n  }, []);\n\n  const announce = useCallback(\n    (message: string, options?: AnnounceOptions) => {\n      if (!message) return;\n      const channel = options?.assertive ? 'assertive' : 'polite';\n      (channel === 'polite' ? politeQueue : assertiveQueue).current.push(message);\n      drain(channel);\n    },\n    [drain],\n  );\n\n  return (\n    \u003CAnnouncerContext.Provider value={{ announce }}>\n      {children}\n      {\u002F* Exactly one polite and one assertive region, mounted once, empty. *\u002F}\n      \u003Cdiv role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {polite}\n      \u003C\u002Fdiv>\n      \u003Cdiv role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\" className=\"sr-only\">\n        {assertive}\n      \u003C\u002Fdiv>\n    \u003C\u002FAnnouncerContext.Provider>\n  );\n}\n\nexport function useAnnouncer(): AnnouncerApi {\n  const ctx = useContext(AnnouncerContext);\n  if (!ctx) {\n    throw new Error('useAnnouncer must be used within an AnnouncerProvider');\n  }\n  return ctx;\n}\n","tsx",[328,723,724,732,738,745,750,755,760,765,770,775,783,796,801,812,827,832,837,847,888,893,898,933,938,974,1007,1034,1039,1046,1067,1085,1111,1116,1151,1165,1195,1221,1240,1259,1264,1277,1283,1295,1308,1317,1323,1335,1348,1360,1370,1376,1382,1387,1402,1428,1446,1472,1497,1506,1512,1518,1524,1529,1538,1555,1561,1572,1617,1623,1633,1668,1674,1683,1693,1698,1703,1708,1727,1743,1756,1775,1781,1789],{"__ignoreMap":415},[419,725,726,730],{"class":421,"line":422},[419,727,729],{"class":728},"sZZnC","'use client'",[419,731,445],{"class":428},[419,733,734],{"class":421,"line":432},[419,735,737],{"emptyLinePlaceholder":736},true,"\n",[419,739,740,743],{"class":421,"line":448},[419,741,742],{"class":459},"import",[419,744,429],{"class":428},[419,746,747],{"class":421,"line":465},[419,748,749],{"class":428},"  createContext,\n",[419,751,752],{"class":421,"line":479},[419,753,754],{"class":428},"  useCallback,\n",[419,756,757],{"class":421,"line":494},[419,758,759],{"class":428},"  useContext,\n",[419,761,762],{"class":421,"line":507},[419,763,764],{"class":428},"  useEffect,\n",[419,766,767],{"class":421,"line":519},[419,768,769],{"class":428},"  useRef,\n",[419,771,772],{"class":421,"line":532},[419,773,774],{"class":428},"  useState,\n",[419,776,777,780],{"class":421,"line":558},[419,778,779],{"class":459},"  type",[419,781,782],{"class":428}," ReactNode,\n",[419,784,785,788,791,794],{"class":421,"line":571},[419,786,787],{"class":428},"} ",[419,789,790],{"class":459},"from",[419,792,793],{"class":728}," 'react'",[419,795,445],{"class":428},[419,797,799],{"class":421,"line":798},12,[419,800,737],{"emptyLinePlaceholder":736},[419,802,804,807,810],{"class":421,"line":803},13,[419,805,806],{"class":459},"interface",[419,808,809],{"class":425}," AnnounceOptions",[419,811,429],{"class":428},[419,813,815,819,822,825],{"class":421,"line":814},14,[419,816,818],{"class":817},"s4XuR","  assertive",[419,820,821],{"class":459},"?:",[419,823,824],{"class":435}," boolean",[419,826,445],{"class":428},[419,828,830],{"class":421,"line":829},15,[419,831,574],{"class":428},[419,833,835],{"class":421,"line":834},16,[419,836,737],{"emptyLinePlaceholder":736},[419,838,840,842,845],{"class":421,"line":839},17,[419,841,806],{"class":459},[419,843,844],{"class":425}," AnnouncerApi",[419,846,429],{"class":428},[419,848,850,853,856,859,862,864,867,870,873,875,877,880,883,886],{"class":421,"line":849},18,[419,851,852],{"class":425},"  announce",[419,854,855],{"class":459},":",[419,857,858],{"class":428}," (",[419,860,861],{"class":817},"message",[419,863,855],{"class":459},[419,865,866],{"class":435}," string",[419,868,869],{"class":428},", ",[419,871,872],{"class":817},"options",[419,874,821],{"class":459},[419,876,809],{"class":425},[419,878,879],{"class":428},") ",[419,881,882],{"class":459},"=>",[419,884,885],{"class":435}," void",[419,887,445],{"class":428},[419,889,891],{"class":421,"line":890},19,[419,892,574],{"class":428},[419,894,896],{"class":421,"line":895},20,[419,897,737],{"emptyLinePlaceholder":736},[419,899,901,904,907,910,913,916,919,922,925,928,931],{"class":421,"line":900},21,[419,902,903],{"class":459},"const",[419,905,906],{"class":435}," AnnouncerContext",[419,908,909],{"class":459}," =",[419,911,912],{"class":425}," createContext",[419,914,915],{"class":428},"\u003C",[419,917,918],{"class":425},"AnnouncerApi",[419,920,921],{"class":459}," |",[419,923,924],{"class":435}," null",[419,926,927],{"class":428},">(",[419,929,930],{"class":435},"null",[419,932,555],{"class":428},[419,934,936],{"class":421,"line":935},22,[419,937,737],{"emptyLinePlaceholder":736},[419,939,941,944,947,950,953,956,959,961,964,966,968,971],{"class":421,"line":940},23,[419,942,943],{"class":459},"export",[419,945,946],{"class":459}," function",[419,948,949],{"class":425}," AnnouncerProvider",[419,951,952],{"class":428},"({ ",[419,954,955],{"class":817},"children",[419,957,958],{"class":428}," }",[419,960,855],{"class":459},[419,962,963],{"class":428}," { ",[419,965,955],{"class":817},[419,967,855],{"class":459},[419,969,970],{"class":425}," ReactNode",[419,972,973],{"class":428}," }) {\n",[419,975,977,980,983,986,988,991,994,997,1000,1002,1005],{"class":421,"line":976},24,[419,978,979],{"class":459},"  const",[419,981,982],{"class":428}," [",[419,984,985],{"class":435},"polite",[419,987,869],{"class":428},[419,989,990],{"class":435},"setPolite",[419,992,993],{"class":428},"] ",[419,995,996],{"class":459},"=",[419,998,999],{"class":425}," useState",[419,1001,543],{"class":428},[419,1003,1004],{"class":728},"''",[419,1006,555],{"class":428},[419,1008,1010,1012,1014,1017,1019,1022,1024,1026,1028,1030,1032],{"class":421,"line":1009},25,[419,1011,979],{"class":459},[419,1013,982],{"class":428},[419,1015,1016],{"class":435},"assertive",[419,1018,869],{"class":428},[419,1020,1021],{"class":435},"setAssertive",[419,1023,993],{"class":428},[419,1025,996],{"class":459},[419,1027,999],{"class":425},[419,1029,543],{"class":428},[419,1031,1004],{"class":728},[419,1033,555],{"class":428},[419,1035,1037],{"class":421,"line":1036},26,[419,1038,737],{"emptyLinePlaceholder":736},[419,1040,1042],{"class":421,"line":1041},27,[419,1043,1045],{"class":1044},"sJ8bj","  \u002F\u002F Refs hold the pending queues so enqueueing never depends on render timing.\n",[419,1047,1049,1051,1054,1056,1059,1061,1064],{"class":421,"line":1048},28,[419,1050,979],{"class":459},[419,1052,1053],{"class":435}," politeQueue",[419,1055,909],{"class":459},[419,1057,1058],{"class":425}," useRef",[419,1060,915],{"class":428},[419,1062,1063],{"class":435},"string",[419,1065,1066],{"class":428},"[]>([]);\n",[419,1068,1070,1072,1075,1077,1079,1081,1083],{"class":421,"line":1069},29,[419,1071,979],{"class":459},[419,1073,1074],{"class":435}," assertiveQueue",[419,1076,909],{"class":459},[419,1078,1058],{"class":425},[419,1080,915],{"class":428},[419,1082,1063],{"class":435},[419,1084,1066],{"class":428},[419,1086,1088,1090,1093,1095,1097,1100,1103,1106,1108],{"class":421,"line":1087},30,[419,1089,979],{"class":459},[419,1091,1092],{"class":435}," draining",[419,1094,909],{"class":459},[419,1096,1058],{"class":425},[419,1098,1099],{"class":428},"({ polite: ",[419,1101,1102],{"class":435},"false",[419,1104,1105],{"class":428},", assertive: ",[419,1107,1102],{"class":435},[419,1109,1110],{"class":428}," });\n",[419,1112,1114],{"class":421,"line":1113},31,[419,1115,737],{"emptyLinePlaceholder":736},[419,1117,1119,1121,1124,1126,1129,1132,1135,1137,1140,1142,1145,1147,1149],{"class":421,"line":1118},32,[419,1120,979],{"class":459},[419,1122,1123],{"class":435}," drain",[419,1125,909],{"class":459},[419,1127,1128],{"class":425}," useCallback",[419,1130,1131],{"class":428},"((",[419,1133,1134],{"class":817},"channel",[419,1136,855],{"class":459},[419,1138,1139],{"class":728}," 'polite'",[419,1141,921],{"class":459},[419,1143,1144],{"class":728}," 'assertive'",[419,1146,879],{"class":428},[419,1148,882],{"class":459},[419,1150,429],{"class":428},[419,1152,1154,1157,1160,1163],{"class":421,"line":1153},33,[419,1155,1156],{"class":459},"    if",[419,1158,1159],{"class":428}," (draining.current[channel]) ",[419,1161,1162],{"class":459},"return",[419,1164,445],{"class":428},[419,1166,1168,1171,1174,1176,1179,1182,1184,1187,1190,1192],{"class":421,"line":1167},34,[419,1169,1170],{"class":459},"    const",[419,1172,1173],{"class":435}," queue",[419,1175,909],{"class":459},[419,1177,1178],{"class":428}," channel ",[419,1180,1181],{"class":459},"===",[419,1183,1139],{"class":728},[419,1185,1186],{"class":459}," ?",[419,1188,1189],{"class":428}," politeQueue ",[419,1191,855],{"class":459},[419,1193,1194],{"class":428}," assertiveQueue;\n",[419,1196,1198,1200,1203,1205,1207,1209,1211,1213,1216,1218],{"class":421,"line":1197},35,[419,1199,1170],{"class":459},[419,1201,1202],{"class":435}," setText",[419,1204,909],{"class":459},[419,1206,1178],{"class":428},[419,1208,1181],{"class":459},[419,1210,1139],{"class":728},[419,1212,1186],{"class":459},[419,1214,1215],{"class":428}," setPolite ",[419,1217,855],{"class":459},[419,1219,1220],{"class":428}," setAssertive;\n",[419,1222,1224,1226,1229,1231,1234,1237],{"class":421,"line":1223},36,[419,1225,1170],{"class":459},[419,1227,1228],{"class":435}," next",[419,1230,909],{"class":459},[419,1232,1233],{"class":428}," queue.current.",[419,1235,1236],{"class":425},"shift",[419,1238,1239],{"class":428},"();\n",[419,1241,1243,1245,1248,1250,1253,1255,1257],{"class":421,"line":1242},37,[419,1244,1156],{"class":459},[419,1246,1247],{"class":428}," (next ",[419,1249,1181],{"class":459},[419,1251,1252],{"class":435}," undefined",[419,1254,879],{"class":428},[419,1256,1162],{"class":459},[419,1258,445],{"class":428},[419,1260,1262],{"class":421,"line":1261},38,[419,1263,737],{"emptyLinePlaceholder":736},[419,1265,1267,1270,1272,1275],{"class":421,"line":1266},39,[419,1268,1269],{"class":428},"    draining.current[channel] ",[419,1271,996],{"class":459},[419,1273,1274],{"class":435}," true",[419,1276,445],{"class":428},[419,1278,1280],{"class":421,"line":1279},40,[419,1281,1282],{"class":1044},"    \u002F\u002F Clear first so an identical next message is still a DOM change (4.1.3).\n",[419,1284,1286,1289,1291,1293],{"class":421,"line":1285},41,[419,1287,1288],{"class":425},"    setText",[419,1290,543],{"class":428},[419,1292,1004],{"class":728},[419,1294,555],{"class":428},[419,1296,1298,1301,1304,1306],{"class":421,"line":1297},42,[419,1299,1300],{"class":425},"    requestAnimationFrame",[419,1302,1303],{"class":428},"(() ",[419,1305,882],{"class":459},[419,1307,429],{"class":428},[419,1309,1311,1314],{"class":421,"line":1310},43,[419,1312,1313],{"class":425},"      setText",[419,1315,1316],{"class":428},"(next);\n",[419,1318,1320],{"class":421,"line":1319},44,[419,1321,1322],{"class":1044},"      \u002F\u002F Give AT time to read before the next item; tune to taste.\n",[419,1324,1326,1329,1331,1333],{"class":421,"line":1325},45,[419,1327,1328],{"class":425},"      setTimeout",[419,1330,1303],{"class":428},[419,1332,882],{"class":459},[419,1334,429],{"class":428},[419,1336,1338,1341,1343,1346],{"class":421,"line":1337},46,[419,1339,1340],{"class":428},"        draining.current[channel] ",[419,1342,996],{"class":459},[419,1344,1345],{"class":435}," false",[419,1347,445],{"class":428},[419,1349,1351,1354,1357],{"class":421,"line":1350},47,[419,1352,1353],{"class":425},"        drain",[419,1355,1356],{"class":428},"(channel); ",[419,1358,1359],{"class":1044},"\u002F\u002F Pull the next queued message, if any.\n",[419,1361,1363,1366,1368],{"class":421,"line":1362},48,[419,1364,1365],{"class":428},"      }, ",[419,1367,631],{"class":435},[419,1369,555],{"class":428},[419,1371,1373],{"class":421,"line":1372},49,[419,1374,1375],{"class":428},"    });\n",[419,1377,1379],{"class":421,"line":1378},50,[419,1380,1381],{"class":428},"  }, []);\n",[419,1383,1385],{"class":421,"line":1384},51,[419,1386,737],{"emptyLinePlaceholder":736},[419,1388,1390,1392,1395,1397,1399],{"class":421,"line":1389},52,[419,1391,979],{"class":459},[419,1393,1394],{"class":435}," announce",[419,1396,909],{"class":459},[419,1398,1128],{"class":425},[419,1400,1401],{"class":428},"(\n",[419,1403,1405,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426],{"class":421,"line":1404},53,[419,1406,1407],{"class":428},"    (",[419,1409,861],{"class":817},[419,1411,855],{"class":459},[419,1413,866],{"class":435},[419,1415,869],{"class":428},[419,1417,872],{"class":817},[419,1419,821],{"class":459},[419,1421,809],{"class":425},[419,1423,879],{"class":428},[419,1425,882],{"class":459},[419,1427,429],{"class":428},[419,1429,1431,1434,1436,1439,1442,1444],{"class":421,"line":1430},54,[419,1432,1433],{"class":459},"      if",[419,1435,858],{"class":428},[419,1437,1438],{"class":459},"!",[419,1440,1441],{"class":428},"message) ",[419,1443,1162],{"class":459},[419,1445,445],{"class":428},[419,1447,1449,1452,1455,1457,1460,1463,1465,1468,1470],{"class":421,"line":1448},55,[419,1450,1451],{"class":459},"      const",[419,1453,1454],{"class":435}," channel",[419,1456,909],{"class":459},[419,1458,1459],{"class":428}," options?.assertive ",[419,1461,1462],{"class":459},"?",[419,1464,1144],{"class":728},[419,1466,1467],{"class":459}," :",[419,1469,1139],{"class":728},[419,1471,445],{"class":428},[419,1473,1475,1478,1480,1482,1484,1486,1488,1491,1494],{"class":421,"line":1474},56,[419,1476,1477],{"class":428},"      (channel ",[419,1479,1181],{"class":459},[419,1481,1139],{"class":728},[419,1483,1186],{"class":459},[419,1485,1189],{"class":428},[419,1487,855],{"class":459},[419,1489,1490],{"class":428}," assertiveQueue).current.",[419,1492,1493],{"class":425},"push",[419,1495,1496],{"class":428},"(message);\n",[419,1498,1500,1503],{"class":421,"line":1499},57,[419,1501,1502],{"class":425},"      drain",[419,1504,1505],{"class":428},"(channel);\n",[419,1507,1509],{"class":421,"line":1508},58,[419,1510,1511],{"class":428},"    },\n",[419,1513,1515],{"class":421,"line":1514},59,[419,1516,1517],{"class":428},"    [drain],\n",[419,1519,1521],{"class":421,"line":1520},60,[419,1522,1523],{"class":428},"  );\n",[419,1525,1527],{"class":421,"line":1526},61,[419,1528,737],{"emptyLinePlaceholder":736},[419,1530,1532,1535],{"class":421,"line":1531},62,[419,1533,1534],{"class":459},"  return",[419,1536,1537],{"class":428}," (\n",[419,1539,1541,1544,1547,1550,1552],{"class":421,"line":1540},63,[419,1542,1543],{"class":428},"    \u003C",[419,1545,1546],{"class":435},"AnnouncerContext.Provider",[419,1548,1549],{"class":425}," value",[419,1551,996],{"class":459},[419,1553,1554],{"class":428},"{{ announce }}>\n",[419,1556,1558],{"class":421,"line":1557},64,[419,1559,1560],{"class":428},"      {children}\n",[419,1562,1564,1567,1570],{"class":421,"line":1563},65,[419,1565,1566],{"class":428},"      {",[419,1568,1569],{"class":1044},"\u002F* Exactly one polite and one assertive region, mounted once, empty. *\u002F",[419,1571,574],{"class":428},[419,1573,1575,1578,1582,1585,1587,1590,1593,1595,1598,1601,1603,1606,1609,1611,1614],{"class":421,"line":1574},66,[419,1576,1577],{"class":428},"      \u003C",[419,1579,1581],{"class":1580},"s9eBZ","div",[419,1583,1584],{"class":425}," role",[419,1586,996],{"class":459},[419,1588,1589],{"class":728},"\"status\"",[419,1591,1592],{"class":425}," aria-live",[419,1594,996],{"class":459},[419,1596,1597],{"class":728},"\"polite\"",[419,1599,1600],{"class":425}," aria-atomic",[419,1602,996],{"class":459},[419,1604,1605],{"class":728},"\"true\"",[419,1607,1608],{"class":425}," className",[419,1610,996],{"class":459},[419,1612,1613],{"class":728},"\"sr-only\"",[419,1615,1616],{"class":428},">\n",[419,1618,1620],{"class":421,"line":1619},67,[419,1621,1622],{"class":428},"        {polite}\n",[419,1624,1626,1629,1631],{"class":421,"line":1625},68,[419,1627,1628],{"class":428},"      \u003C\u002F",[419,1630,1581],{"class":1580},[419,1632,1616],{"class":428},[419,1634,1636,1638,1640,1642,1644,1647,1649,1651,1654,1656,1658,1660,1662,1664,1666],{"class":421,"line":1635},69,[419,1637,1577],{"class":428},[419,1639,1581],{"class":1580},[419,1641,1584],{"class":425},[419,1643,996],{"class":459},[419,1645,1646],{"class":728},"\"alert\"",[419,1648,1592],{"class":425},[419,1650,996],{"class":459},[419,1652,1653],{"class":728},"\"assertive\"",[419,1655,1600],{"class":425},[419,1657,996],{"class":459},[419,1659,1605],{"class":728},[419,1661,1608],{"class":425},[419,1663,996],{"class":459},[419,1665,1613],{"class":728},[419,1667,1616],{"class":428},[419,1669,1671],{"class":421,"line":1670},70,[419,1672,1673],{"class":428},"        {assertive}\n",[419,1675,1677,1679,1681],{"class":421,"line":1676},71,[419,1678,1628],{"class":428},[419,1680,1581],{"class":1580},[419,1682,1616],{"class":428},[419,1684,1686,1689,1691],{"class":421,"line":1685},72,[419,1687,1688],{"class":428},"    \u003C\u002F",[419,1690,1546],{"class":435},[419,1692,1616],{"class":428},[419,1694,1696],{"class":421,"line":1695},73,[419,1697,1523],{"class":428},[419,1699,1701],{"class":421,"line":1700},74,[419,1702,574],{"class":428},[419,1704,1706],{"class":421,"line":1705},75,[419,1707,737],{"emptyLinePlaceholder":736},[419,1709,1711,1713,1715,1718,1721,1723,1725],{"class":421,"line":1710},76,[419,1712,943],{"class":459},[419,1714,946],{"class":459},[419,1716,1717],{"class":425}," useAnnouncer",[419,1719,1720],{"class":428},"()",[419,1722,855],{"class":459},[419,1724,844],{"class":425},[419,1726,429],{"class":428},[419,1728,1730,1732,1735,1737,1740],{"class":421,"line":1729},77,[419,1731,979],{"class":459},[419,1733,1734],{"class":435}," ctx",[419,1736,909],{"class":459},[419,1738,1739],{"class":425}," useContext",[419,1741,1742],{"class":428},"(AnnouncerContext);\n",[419,1744,1746,1749,1751,1753],{"class":421,"line":1745},78,[419,1747,1748],{"class":459},"  if",[419,1750,858],{"class":428},[419,1752,1438],{"class":459},[419,1754,1755],{"class":428},"ctx) {\n",[419,1757,1759,1762,1765,1768,1770,1773],{"class":421,"line":1758},79,[419,1760,1761],{"class":459},"    throw",[419,1763,1764],{"class":459}," new",[419,1766,1767],{"class":425}," Error",[419,1769,543],{"class":428},[419,1771,1772],{"class":728},"'useAnnouncer must be used within an AnnouncerProvider'",[419,1774,555],{"class":428},[419,1776,1778],{"class":421,"line":1777},80,[419,1779,1780],{"class":428},"  }\n",[419,1782,1784,1786],{"class":421,"line":1783},81,[419,1785,1534],{"class":459},[419,1787,1788],{"class":428}," ctx;\n",[419,1790,1792],{"class":421,"line":1791},82,[419,1793,574],{"class":428},[324,1795,1796,1797,858,1800,1803,1804,1807,1808,1811,1812,1815],{},"Two design points carry the weight. ",[368,1798,1799],{},"Clearing before setting",[328,1801,1802],{},"setText('')"," then a framed ",[328,1805,1806],{},"setText(next)",") guarantees a DOM mutation even when the same string is announced twice, fixing the duplicate-message silence. ",[368,1809,1810],{},"The ref-backed FIFO queue"," serializes rapid calls so a burst of announcements is read in order instead of clobbering down to the last one. The brief ",[328,1813,1814],{},"setTimeout"," between drains gives screen readers room to finish speaking; calibrate it to your message lengths.",[354,1817],{},[357,1819,1821],{"id":1820},"ssr-safety","SSR Safety",[324,1823,1824,1825,1827,1828,1831,1832,1834,1835,1837,1838,1841],{},"The provider is a client component (",[328,1826,729],{},") and reads no browser globals during render—",[328,1829,1830],{},"requestAnimationFrame"," and ",[328,1833,1814],{}," only run inside effects\u002Fcallbacks, never at module or render time, so there is nothing to crash the server pass. The two live-region ",[328,1836,1581],{},"s render identically on server and client, which means ",[368,1839,1840],{},"no hydration mismatch",": they serialize as empty regions and the client takes over with empty state. Mount the provider once at the App Router root:",[410,1843,1845],{"className":719,"code":1844,"language":721,"meta":415,"style":415},"\u002F\u002F app\u002Flayout.tsx\nimport { AnnouncerProvider } from '@\u002Fcomponents\u002Fannouncer';\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\n      \u003Cbody>\n        {\u002F* One provider wraps everything; regions exist from first paint. *\u002F}\n        \u003CAnnouncerProvider>{children}\u003C\u002FAnnouncerProvider>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}\n",[328,1846,1847,1852,1866,1870,1906,1912,1929,1938,1948,1963,1971,1979,1983],{"__ignoreMap":415},[419,1848,1849],{"class":421,"line":422},[419,1850,1851],{"class":1044},"\u002F\u002F app\u002Flayout.tsx\n",[419,1853,1854,1856,1859,1861,1864],{"class":421,"line":432},[419,1855,742],{"class":459},[419,1857,1858],{"class":428}," { AnnouncerProvider } ",[419,1860,790],{"class":459},[419,1862,1863],{"class":728}," '@\u002Fcomponents\u002Fannouncer'",[419,1865,445],{"class":428},[419,1867,1868],{"class":421,"line":448},[419,1869,737],{"emptyLinePlaceholder":736},[419,1871,1872,1874,1877,1879,1882,1884,1886,1888,1890,1892,1894,1896,1899,1901,1904],{"class":421,"line":465},[419,1873,943],{"class":459},[419,1875,1876],{"class":459}," default",[419,1878,946],{"class":459},[419,1880,1881],{"class":425}," RootLayout",[419,1883,952],{"class":428},[419,1885,955],{"class":817},[419,1887,958],{"class":428},[419,1889,855],{"class":459},[419,1891,963],{"class":428},[419,1893,955],{"class":817},[419,1895,855],{"class":459},[419,1897,1898],{"class":425}," React",[419,1900,352],{"class":428},[419,1902,1903],{"class":425},"ReactNode",[419,1905,973],{"class":428},[419,1907,1908,1910],{"class":421,"line":479},[419,1909,1534],{"class":459},[419,1911,1537],{"class":428},[419,1913,1914,1916,1919,1922,1924,1927],{"class":421,"line":494},[419,1915,1543],{"class":428},[419,1917,1918],{"class":1580},"html",[419,1920,1921],{"class":425}," lang",[419,1923,996],{"class":459},[419,1925,1926],{"class":728},"\"en\"",[419,1928,1616],{"class":428},[419,1930,1931,1933,1936],{"class":421,"line":507},[419,1932,1577],{"class":428},[419,1934,1935],{"class":1580},"body",[419,1937,1616],{"class":428},[419,1939,1940,1943,1946],{"class":421,"line":519},[419,1941,1942],{"class":428},"        {",[419,1944,1945],{"class":1044},"\u002F* One provider wraps everything; regions exist from first paint. *\u002F",[419,1947,574],{"class":428},[419,1949,1950,1953,1956,1959,1961],{"class":421,"line":532},[419,1951,1952],{"class":428},"        \u003C",[419,1954,1955],{"class":435},"AnnouncerProvider",[419,1957,1958],{"class":428},">{children}\u003C\u002F",[419,1960,1955],{"class":435},[419,1962,1616],{"class":428},[419,1964,1965,1967,1969],{"class":421,"line":558},[419,1966,1628],{"class":428},[419,1968,1935],{"class":1580},[419,1970,1616],{"class":428},[419,1972,1973,1975,1977],{"class":421,"line":571},[419,1974,1688],{"class":428},[419,1976,1918],{"class":1580},[419,1978,1616],{"class":428},[419,1980,1981],{"class":421,"line":798},[419,1982,1523],{"class":428},[419,1984,1985],{"class":421,"line":803},[419,1986,574],{"class":428},[324,1988,1989],{},"Consuming it anywhere is a one-liner:",[410,1991,1993],{"className":719,"code":1992,"language":721,"meta":415,"style":415},"'use client';\n\nimport { useAnnouncer } from '@\u002Fcomponents\u002Fannouncer';\n\nexport function SaveButton() {\n  const { announce } = useAnnouncer();\n  return (\n    \u003Cbutton\n      onClick={async () => {\n        await saveDraft();\n        announce('Draft saved'); \u002F\u002F Polite by default.\n      }}\n    >\n      Save\n    \u003C\u002Fbutton>\n  );\n}\n\n\u002F\u002F For errors, opt into the assertive channel:\n\u002F\u002F announce('Save failed—check your connection', { assertive: true });\n",[328,1994,1995,2001,2005,2018,2022,2034,2051,2057,2064,2084,2094,2110,2115,2120,2125,2134,2138,2142,2146,2151],{"__ignoreMap":415},[419,1996,1997,1999],{"class":421,"line":422},[419,1998,729],{"class":728},[419,2000,445],{"class":428},[419,2002,2003],{"class":421,"line":432},[419,2004,737],{"emptyLinePlaceholder":736},[419,2006,2007,2009,2012,2014,2016],{"class":421,"line":448},[419,2008,742],{"class":459},[419,2010,2011],{"class":428}," { useAnnouncer } ",[419,2013,790],{"class":459},[419,2015,1863],{"class":728},[419,2017,445],{"class":428},[419,2019,2020],{"class":421,"line":465},[419,2021,737],{"emptyLinePlaceholder":736},[419,2023,2024,2026,2028,2031],{"class":421,"line":479},[419,2025,943],{"class":459},[419,2027,946],{"class":459},[419,2029,2030],{"class":425}," SaveButton",[419,2032,2033],{"class":428},"() {\n",[419,2035,2036,2038,2040,2042,2045,2047,2049],{"class":421,"line":494},[419,2037,979],{"class":459},[419,2039,963],{"class":428},[419,2041,715],{"class":435},[419,2043,2044],{"class":428}," } ",[419,2046,996],{"class":459},[419,2048,1717],{"class":425},[419,2050,1239],{"class":428},[419,2052,2053,2055],{"class":421,"line":507},[419,2054,1534],{"class":459},[419,2056,1537],{"class":428},[419,2058,2059,2061],{"class":421,"line":519},[419,2060,1543],{"class":428},[419,2062,2063],{"class":1580},"button\n",[419,2065,2066,2069,2071,2074,2077,2080,2082],{"class":421,"line":532},[419,2067,2068],{"class":425},"      onClick",[419,2070,996],{"class":459},[419,2072,2073],{"class":428},"{",[419,2075,2076],{"class":459},"async",[419,2078,2079],{"class":428}," () ",[419,2081,882],{"class":459},[419,2083,429],{"class":428},[419,2085,2086,2089,2092],{"class":421,"line":558},[419,2087,2088],{"class":459},"        await",[419,2090,2091],{"class":425}," saveDraft",[419,2093,1239],{"class":428},[419,2095,2096,2099,2101,2104,2107],{"class":421,"line":571},[419,2097,2098],{"class":425},"        announce",[419,2100,543],{"class":428},[419,2102,2103],{"class":728},"'Draft saved'",[419,2105,2106],{"class":428},"); ",[419,2108,2109],{"class":1044},"\u002F\u002F Polite by default.\n",[419,2111,2112],{"class":421,"line":798},[419,2113,2114],{"class":428},"      }}\n",[419,2116,2117],{"class":421,"line":803},[419,2118,2119],{"class":428},"    >\n",[419,2121,2122],{"class":421,"line":814},[419,2123,2124],{"class":428},"      Save\n",[419,2126,2127,2129,2132],{"class":421,"line":829},[419,2128,1688],{"class":428},[419,2130,2131],{"class":1580},"button",[419,2133,1616],{"class":428},[419,2135,2136],{"class":421,"line":834},[419,2137,1523],{"class":428},[419,2139,2140],{"class":421,"line":839},[419,2141,574],{"class":428},[419,2143,2144],{"class":421,"line":849},[419,2145,737],{"emptyLinePlaceholder":736},[419,2147,2148],{"class":421,"line":890},[419,2149,2150],{"class":1044},"\u002F\u002F For errors, opt into the assertive channel:\n",[419,2152,2153],{"class":421,"line":895},[419,2154,2155],{"class":1044},"\u002F\u002F announce('Save failed—check your connection', { assertive: true });\n",[354,2157],{},[357,2159,2161],{"id":2160},"how-to-verify","How to Verify",[324,2163,2164,2167],{},[368,2165,2166],{},"axe \u002F jest-axe."," Confirm both regions exist with correct roles and that a queued message reaches the DOM. Use fake timers to advance past the drain delay:",[410,2169,2171],{"className":719,"code":2170,"language":721,"meta":415,"style":415},"import { render, screen, act } from '@testing-library\u002Freact';\nimport { axe } from 'jest-axe';\nimport { AnnouncerProvider, useAnnouncer } from '.\u002Fannouncer';\n\nfunction Probe() {\n  const { announce } = useAnnouncer();\n  return \u003Cbutton onClick={() => announce('Item added')}>Add\u003C\u002Fbutton>;\n}\n\ntest('announces queued message with no axe violations', async () => {\n  const { container } = render(\n    \u003CAnnouncerProvider>\n      \u003CProbe \u002F>\n    \u003C\u002FAnnouncerProvider>,\n  );\n  await act(async () => {\n    screen.getByText('Add').click();\n  });\n  \u002F\u002F requestAnimationFrame + timer flush, then assert:\n  expect(screen.getByRole('status')).toBeInTheDocument();\n  expect(screen.getByRole('alert')).toBeInTheDocument();\n  expect(await axe(container)).toHaveNoViolations();\n});\n",[328,2172,2173,2187,2201,2215,2219,2229,2245,2279,2283,2287,2307,2325,2333,2343,2352,2356,2374,2395,2400,2405,2429,2448,2468],{"__ignoreMap":415},[419,2174,2175,2177,2180,2182,2185],{"class":421,"line":422},[419,2176,742],{"class":459},[419,2178,2179],{"class":428}," { render, screen, act } ",[419,2181,790],{"class":459},[419,2183,2184],{"class":728}," '@testing-library\u002Freact'",[419,2186,445],{"class":428},[419,2188,2189,2191,2194,2196,2199],{"class":421,"line":432},[419,2190,742],{"class":459},[419,2192,2193],{"class":428}," { axe } ",[419,2195,790],{"class":459},[419,2197,2198],{"class":728}," 'jest-axe'",[419,2200,445],{"class":428},[419,2202,2203,2205,2208,2210,2213],{"class":421,"line":448},[419,2204,742],{"class":459},[419,2206,2207],{"class":428}," { AnnouncerProvider, useAnnouncer } ",[419,2209,790],{"class":459},[419,2211,2212],{"class":728}," '.\u002Fannouncer'",[419,2214,445],{"class":428},[419,2216,2217],{"class":421,"line":465},[419,2218,737],{"emptyLinePlaceholder":736},[419,2220,2221,2224,2227],{"class":421,"line":479},[419,2222,2223],{"class":459},"function",[419,2225,2226],{"class":425}," Probe",[419,2228,2033],{"class":428},[419,2230,2231,2233,2235,2237,2239,2241,2243],{"class":421,"line":494},[419,2232,979],{"class":459},[419,2234,963],{"class":428},[419,2236,715],{"class":435},[419,2238,2044],{"class":428},[419,2240,996],{"class":459},[419,2242,1717],{"class":425},[419,2244,1239],{"class":428},[419,2246,2247,2249,2252,2254,2257,2259,2262,2264,2266,2268,2271,2274,2276],{"class":421,"line":507},[419,2248,1534],{"class":459},[419,2250,2251],{"class":428}," \u003C",[419,2253,2131],{"class":1580},[419,2255,2256],{"class":425}," onClick",[419,2258,996],{"class":459},[419,2260,2261],{"class":428},"{() ",[419,2263,882],{"class":459},[419,2265,1394],{"class":425},[419,2267,543],{"class":428},[419,2269,2270],{"class":728},"'Item added'",[419,2272,2273],{"class":428},")}>Add\u003C\u002F",[419,2275,2131],{"class":1580},[419,2277,2278],{"class":428},">;\n",[419,2280,2281],{"class":421,"line":519},[419,2282,574],{"class":428},[419,2284,2285],{"class":421,"line":532},[419,2286,737],{"emptyLinePlaceholder":736},[419,2288,2289,2292,2294,2297,2299,2301,2303,2305],{"class":421,"line":558},[419,2290,2291],{"class":425},"test",[419,2293,543],{"class":428},[419,2295,2296],{"class":728},"'announces queued message with no axe violations'",[419,2298,869],{"class":428},[419,2300,2076],{"class":459},[419,2302,2079],{"class":428},[419,2304,882],{"class":459},[419,2306,429],{"class":428},[419,2308,2309,2311,2313,2316,2318,2320,2323],{"class":421,"line":571},[419,2310,979],{"class":459},[419,2312,963],{"class":428},[419,2314,2315],{"class":435},"container",[419,2317,2044],{"class":428},[419,2319,996],{"class":459},[419,2321,2322],{"class":425}," render",[419,2324,1401],{"class":428},[419,2326,2327,2329,2331],{"class":421,"line":798},[419,2328,1543],{"class":428},[419,2330,1955],{"class":435},[419,2332,1616],{"class":428},[419,2334,2335,2337,2340],{"class":421,"line":803},[419,2336,1577],{"class":428},[419,2338,2339],{"class":435},"Probe",[419,2341,2342],{"class":428}," \u002F>\n",[419,2344,2345,2347,2349],{"class":421,"line":814},[419,2346,1688],{"class":428},[419,2348,1955],{"class":435},[419,2350,2351],{"class":428},">,\n",[419,2353,2354],{"class":421,"line":829},[419,2355,1523],{"class":428},[419,2357,2358,2361,2364,2366,2368,2370,2372],{"class":421,"line":834},[419,2359,2360],{"class":459},"  await",[419,2362,2363],{"class":425}," act",[419,2365,543],{"class":428},[419,2367,2076],{"class":459},[419,2369,2079],{"class":428},[419,2371,882],{"class":459},[419,2373,429],{"class":428},[419,2375,2376,2379,2382,2384,2387,2390,2393],{"class":421,"line":839},[419,2377,2378],{"class":428},"    screen.",[419,2380,2381],{"class":425},"getByText",[419,2383,543],{"class":428},[419,2385,2386],{"class":728},"'Add'",[419,2388,2389],{"class":428},").",[419,2391,2392],{"class":425},"click",[419,2394,1239],{"class":428},[419,2396,2397],{"class":421,"line":849},[419,2398,2399],{"class":428},"  });\n",[419,2401,2402],{"class":421,"line":890},[419,2403,2404],{"class":1044},"  \u002F\u002F requestAnimationFrame + timer flush, then assert:\n",[419,2406,2407,2410,2413,2416,2418,2421,2424,2427],{"class":421,"line":895},[419,2408,2409],{"class":425},"  expect",[419,2411,2412],{"class":428},"(screen.",[419,2414,2415],{"class":425},"getByRole",[419,2417,543],{"class":428},[419,2419,2420],{"class":728},"'status'",[419,2422,2423],{"class":428},")).",[419,2425,2426],{"class":425},"toBeInTheDocument",[419,2428,1239],{"class":428},[419,2430,2431,2433,2435,2437,2439,2442,2444,2446],{"class":421,"line":900},[419,2432,2409],{"class":425},[419,2434,2412],{"class":428},[419,2436,2415],{"class":425},[419,2438,543],{"class":428},[419,2440,2441],{"class":728},"'alert'",[419,2443,2423],{"class":428},[419,2445,2426],{"class":425},[419,2447,1239],{"class":428},[419,2449,2450,2452,2454,2457,2460,2463,2466],{"class":421,"line":935},[419,2451,2409],{"class":425},[419,2453,543],{"class":428},[419,2455,2456],{"class":459},"await",[419,2458,2459],{"class":425}," axe",[419,2461,2462],{"class":428},"(container)).",[419,2464,2465],{"class":425},"toHaveNoViolations",[419,2467,1239],{"class":428},[419,2469,2470],{"class":421,"line":940},[419,2471,2472],{"class":428},"});\n",[324,2474,2475,2476,352],{},"For comprehensive queue and clearing assertions—including identical-message re-announcement and timer control—follow the patterns in ",[341,2477,73],{"href":2478},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library\u002F",[324,2480,2481,2484],{},[368,2482,2483],{},"Screen reader."," With NVDA + Firefox and VoiceOver + Safari: trigger the same message twice and confirm it is spoken both times (proves clearing works); fire three messages in quick succession and confirm all three are read in order (proves queueing); fire an assertive message and confirm it interrupts.",[324,2486,2487,2490,2491,352],{},[368,2488,2489],{},"Keyboard."," The hook itself moves no focus, which is correct—announcements must never steal focus. Confirm that triggering an announcement from a button leaves focus on that button and does not disrupt an in-progress task, consistent with ",[328,2492,351],{},[354,2494],{},[357,2496,2498],{"id":2497},"common-a11y-mistakes","Common a11y Mistakes",[362,2500,2501,2507,2513,2519,2525,2534],{},[365,2502,2503,2506],{},[368,2504,2505],{},"Rendering regions only when there is a message."," Conditionally mounting the region means it is registered too late and the first announcement is missed. Always render both regions; toggle only their text.",[365,2508,2509,2512],{},[368,2510,2511],{},"Skipping the clear step."," Writing the same string twice produces no mutation and no speech. Clear, then set on the next frame.",[365,2514,2515,2518],{},[368,2516,2517],{},"No queue."," Rapid announcements overwrite each other; only the last survives. Serialize through a FIFO queue.",[365,2520,2521,2524],{},[368,2522,2523],{},"Two regions for one purpose."," Multiple polite regions cause stuttering and unpredictable ordering. Centralize to exactly one polite and one assertive region.",[365,2526,2527,2530,2531,2533],{},[368,2528,2529],{},"Defaulting to assertive."," Routine status should be polite; assertive interrupts and desensitizes users. Make ",[328,2532,1016],{}," an explicit opt-in.",[365,2535,2536,2543],{},[368,2537,2538,2539,2542],{},"Touching ",[328,2540,2541],{},"window"," during render."," Reading browser globals at render or module scope breaks SSR. Keep all timing in effects and callbacks.",[354,2545],{},[357,2547,2549],{"id":2548},"conclusion","Conclusion",[324,2551,385,2552,2554,2555,2557],{},[328,2553,334],{}," hook turns live-region correctness into a single, testable concern. Mount one polite and one assertive region at the root, queue messages to prevent clobbering, clear before each set so duplicates still announce, and keep every browser call inside effects for SSR safety. Components then announce state changes with one line and inherit ",[328,2556,351],{}," compliance for free.",[354,2559],{},[357,2561,2563],{"id":2562},"frequently-asked-questions","Frequently Asked Questions",[324,2565,2566],{},[368,2567,2568],{},"Why doesn't my screen reader announce the same message twice in a row?",[324,2570,2571],{},"Setting a live region's text to a string identical to its current value produces no DOM mutation, so assistive technology has nothing to detect and stays silent. Fix it by clearing the region to an empty string first, then writing the message back on the next animation frame. The empty-to-string transition is a real change, so the message is announced even when it repeats.",[324,2573,2574],{},[368,2575,2576],{},"Why use a single provider instead of placing live regions in each component?",[324,2578,2579,2580,2582],{},"Multiple live regions of the same politeness cause stuttering, unpredictable ordering, and missed announcements, and each scattered region risks being mounted too late to register. One provider mounting exactly one polite and one assertive region at the root—exposed through a ",[328,2581,334],{}," hook—gives every component a single, ordered, reliable channel and removes live-region mechanics from feature code.",[324,2584,2585],{},[368,2586,2587],{},"How does the hook handle several announcements fired at once?",[324,2589,2590],{},"It pushes each message onto a FIFO queue held in a ref and drains them one at a time, clearing the region and setting the next message after a short delay. Without this serialization, announcements made in the same render cycle overwrite one another and only the last is spoken. The queue guarantees a burst of updates is read in full and in order.",[324,2592,2593],{},[368,2594,2595],{},"Is the useAnnouncer hook safe for server-side rendering?",[324,2597,2598,2599,1831,2601,2603],{},"Yes. The provider is a client component and reads no browser globals during render—",[328,2600,1830],{},[328,2602,1814],{}," run only inside effects and event callbacks. Both live regions render as empty on the server and hydrate to empty state on the client, so there is no hydration mismatch. Mount the provider once at your App Router root layout.",[354,2605],{},[357,2607,2609],{"id":2608},"related-guides","Related guides",[362,2611,2612,2616,2621,2626],{},[365,2613,2614],{},[341,2615,181],{"href":343},[365,2617,2618],{},[341,2619,193],{"href":2620},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002F",[365,2622,2623],{},[341,2624,199],{"href":2625},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers\u002F",[365,2627,2628],{},[341,2629,73],{"href":2478},[2631,2632,2633],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":415,"searchDepth":432,"depth":432,"links":2635},[2636,2637,2638,2639,2640,2641,2642,2643,2644],{"id":359,"depth":432,"text":360},{"id":579,"depth":432,"text":580},{"id":708,"depth":432,"text":709},{"id":1820,"depth":432,"text":1821},{"id":2160,"depth":432,"text":2161},{"id":2497,"depth":432,"text":2498},{"id":2548,"depth":432,"text":2549},{"id":2562,"depth":432,"text":2563},{"id":2608,"depth":432,"text":2609},null,"Encapsulate aria-live announcements in one reusable React hook—a single polite\u002Fassertive region, message queueing, and clearing to avoid duplicate or dropped announcements.","md",{},false,{"title":187,"description":2646},"9IdQXcP-LrbJlu8ajTjxHs4HIqN6UbnMpB_vNZyZzBQ",[2653,2692,2693,2756],{"title":5,"path":6,"stem":7,"children":2654},[2655,2656,2659,2662,2668,2674,2683,2689],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2657},[2658],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2660},[2661],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2663},[2664,2665],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2666},[2667],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2669},[2670,2671],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2672},[2673],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2675},[2676,2677,2680],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2678},[2679],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2681},[2682],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2684},[2685,2686],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2687},[2688],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2690},[2691],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2694},[2695,2696,2702,2714,2726,2729,2738,2750],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2697},[2698,2699],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2700},[2701],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2703},[2704,2705,2708,2711],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2706},[2707],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2709},[2710],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2712},[2713],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2715},[2716,2717,2720,2723],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2718},[2719],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2721},[2722],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2724},[2725],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2727},[2728],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2730},[2731,2732,2735],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2733},[2734],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2736},[2737],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2739},[2740,2741,2744,2747],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2742},[2743],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2745},[2746],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2748},[2749],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2751},[2752,2753],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2754},[2755],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2757},[2758,2759,2768,2777,2786,2795],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2760},[2761,2762,2765],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2763},[2764],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2766},[2767],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2769},[2770,2771,2774],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2772},[2773],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2775},[2776],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2778},[2779,2780,2783],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2781},[2782],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2784},[2785],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2787},[2788,2789,2792],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2790},[2791],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2793},[2794],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2796},[2797,2798,2801],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2799},[2800],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2802},[2803],{"title":309,"path":310,"stem":311},1781785523940]