[{"data":1,"prerenderedAt":2110},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002F":314,"content-navigation":1958},[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":55,"body":316,"date":1951,"description":1952,"extension":1953,"image":1951,"meta":1954,"modifiedAt":1951,"navigation":425,"noindex":1955,"path":56,"publishedAt":1951,"seo":1956,"stem":57,"updatedAt":1951,"__hash__":1957},"content\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002Findex.md",{"type":317,"value":318,"toc":1940},"minimark",[319,323,341,344,349,352,364,366,370,377,398,770,777,783,928,930,934,944,960,1091,1098,1100,1104,1119,1126,1481,1494,1496,1500,1515,1527,1529,1533,1542,1557,1574,1580,1586,1780,1782,1786,1847,1849,1853,1872,1895,1909,1915,1917,1921,1936],[320,321,55],"h1",{"id":322},"accessible-loading-skeletons-and-spinners",[324,325,326,327,331,332,335,336,340],"p",{},"Loading indicators are deceptively hard to get right. A spinning circle or a shimmering skeleton looks finished, but it usually communicates nothing to a screen reader and frequently animates in ways that distress motion-sensitive users. The fix is to separate two concerns the visuals conflate: the ",[328,329,330],"em",{},"announcement"," that work is happening, which belongs to assistive technology, and the ",[328,333,334],{},"decoration"," that suggests it visually, which should be hidden from assistive technology and silenced under reduced motion. This guide implements both, building on ",[337,338,49],"a",{"href":339},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002F",".",[342,343],"hr",{},[345,346,348],"h2",{"id":347},"prerequisites","Prerequisites",[324,350,351],{},"You should be comfortable rendering conditional UI in React and writing CSS keyframes. Familiarity with ARIA live regions helps but is not required—the relevant attributes are explained inline. The patterns are framework-agnostic in spirit; the React and CSS here translate directly to Vue, Svelte, or Angular.",[324,353,354,355,359,360,363],{},"The governing success criterion is ",[356,357,358],"code",{},"4.1.3 Status Messages"," (Level AA): a change of status—such as content beginning to load—must be programmatically exposed to assistive technology without moving focus. Loading states are the textbook example, and a ",[356,361,362],{},"role=\"status\""," live region is the textbook implementation.",[342,365],{},[345,367,369],{"id":368},"announcing-the-loading-state","Announcing the Loading State",[324,371,372,373,376],{},"A screen-reader user perceives no spinner. If the only signal that data is loading is a rotating SVG, that user is left in silence wondering whether their action registered. The announcement must therefore live in the accessibility tree, delivered through a polite live region with ",[356,374,375],{},"sr-only"," text.",[324,378,379,381,382,385,386,389,390,393,394,397],{},[356,380,362],{}," carries an implicit ",[356,383,384],{},"aria-live=\"polite\"",", so the screen reader queues the message after the current utterance rather than interrupting. Pair it with ",[356,387,388],{},"aria-busy=\"true\""," on the region that is being populated, which tells assistive technology the area is mid-update and its contents are not yet stable. Flip ",[356,391,392],{},"aria-busy"," to ",[356,395,396],{},"false"," and clear the status text when the data arrives.",[399,400,405],"pre",{"className":401,"code":402,"language":403,"meta":404,"style":404},"language-tsx shiki shiki-themes github-light github-dark","'use client';\n\nimport { useReducedMotion } from '.\u002FuseReducedMotion';\n\nexport function LoadingPanel({\n  loading,\n  children,\n}: {\n  loading: boolean;\n  children: React.ReactNode;\n}) {\n  const reduced = useReducedMotion();\n\n  return (\n    \u002F\u002F aria-busy tells AT this region is updating; it flips false when done. (4.1.3)\n    \u003Csection aria-busy={loading}>\n      {loading ? (\n        \u003C>\n          {\u002F* Decorative motion — hidden from screen readers entirely. *\u002F}\n          \u003Cspan\n            className={reduced ? 'spinner spinner--static' : 'spinner'}\n            aria-hidden=\"true\"\n          \u002F>\n          {\u002F* The real announcement: polite, off-screen, focus-preserving. *\u002F}\n          \u003Cp role=\"status\" className=\"sr-only\">\n            Loading content, please wait.\n          \u003C\u002Fp>\n        \u003C\u002F>\n      ) : (\n        children\n      )}\n    \u003C\u002Fsection>\n  );\n}\n","tsx","",[356,406,407,420,427,445,450,466,476,484,496,509,526,532,550,555,564,571,590,601,607,619,628,652,663,669,679,705,711,721,727,737,743,749,759,765],{"__ignoreMap":404},[408,409,412,416],"span",{"class":410,"line":411},"line",1,[408,413,415],{"class":414},"sZZnC","'use client'",[408,417,419],{"class":418},"sVt8B",";\n",[408,421,423],{"class":410,"line":422},2,[408,424,426],{"emptyLinePlaceholder":425},true,"\n",[408,428,430,434,437,440,443],{"class":410,"line":429},3,[408,431,433],{"class":432},"szBVR","import",[408,435,436],{"class":418}," { useReducedMotion } ",[408,438,439],{"class":432},"from",[408,441,442],{"class":414}," '.\u002FuseReducedMotion'",[408,444,419],{"class":418},[408,446,448],{"class":410,"line":447},4,[408,449,426],{"emptyLinePlaceholder":425},[408,451,453,456,459,463],{"class":410,"line":452},5,[408,454,455],{"class":432},"export",[408,457,458],{"class":432}," function",[408,460,462],{"class":461},"sScJk"," LoadingPanel",[408,464,465],{"class":418},"({\n",[408,467,469,473],{"class":410,"line":468},6,[408,470,472],{"class":471},"s4XuR","  loading",[408,474,475],{"class":418},",\n",[408,477,479,482],{"class":410,"line":478},7,[408,480,481],{"class":471},"  children",[408,483,475],{"class":418},[408,485,487,490,493],{"class":410,"line":486},8,[408,488,489],{"class":418},"}",[408,491,492],{"class":432},":",[408,494,495],{"class":418}," {\n",[408,497,499,501,503,507],{"class":410,"line":498},9,[408,500,472],{"class":471},[408,502,492],{"class":432},[408,504,506],{"class":505},"sj4cs"," boolean",[408,508,419],{"class":418},[408,510,512,514,516,519,521,524],{"class":410,"line":511},10,[408,513,481],{"class":471},[408,515,492],{"class":432},[408,517,518],{"class":461}," React",[408,520,340],{"class":418},[408,522,523],{"class":461},"ReactNode",[408,525,419],{"class":418},[408,527,529],{"class":410,"line":528},11,[408,530,531],{"class":418},"}) {\n",[408,533,535,538,541,544,547],{"class":410,"line":534},12,[408,536,537],{"class":432},"  const",[408,539,540],{"class":505}," reduced",[408,542,543],{"class":432}," =",[408,545,546],{"class":461}," useReducedMotion",[408,548,549],{"class":418},"();\n",[408,551,553],{"class":410,"line":552},13,[408,554,426],{"emptyLinePlaceholder":425},[408,556,558,561],{"class":410,"line":557},14,[408,559,560],{"class":432},"  return",[408,562,563],{"class":418}," (\n",[408,565,567],{"class":410,"line":566},15,[408,568,570],{"class":569},"sJ8bj","    \u002F\u002F aria-busy tells AT this region is updating; it flips false when done. (4.1.3)\n",[408,572,574,577,581,584,587],{"class":410,"line":573},16,[408,575,576],{"class":418},"    \u003C",[408,578,580],{"class":579},"s9eBZ","section",[408,582,583],{"class":461}," aria-busy",[408,585,586],{"class":432},"=",[408,588,589],{"class":418},"{loading}>\n",[408,591,593,596,599],{"class":410,"line":592},17,[408,594,595],{"class":418},"      {loading ",[408,597,598],{"class":432},"?",[408,600,563],{"class":418},[408,602,604],{"class":410,"line":603},18,[408,605,606],{"class":418},"        \u003C>\n",[408,608,610,613,616],{"class":410,"line":609},19,[408,611,612],{"class":418},"          {",[408,614,615],{"class":569},"\u002F* Decorative motion — hidden from screen readers entirely. *\u002F",[408,617,618],{"class":418},"}\n",[408,620,622,625],{"class":410,"line":621},20,[408,623,624],{"class":418},"          \u003C",[408,626,627],{"class":579},"span\n",[408,629,631,634,636,639,641,644,647,650],{"class":410,"line":630},21,[408,632,633],{"class":461},"            className",[408,635,586],{"class":432},[408,637,638],{"class":418},"{reduced ",[408,640,598],{"class":432},[408,642,643],{"class":414}," 'spinner spinner--static'",[408,645,646],{"class":432}," :",[408,648,649],{"class":414}," 'spinner'",[408,651,618],{"class":418},[408,653,655,658,660],{"class":410,"line":654},22,[408,656,657],{"class":461},"            aria-hidden",[408,659,586],{"class":432},[408,661,662],{"class":414},"\"true\"\n",[408,664,666],{"class":410,"line":665},23,[408,667,668],{"class":418},"          \u002F>\n",[408,670,672,674,677],{"class":410,"line":671},24,[408,673,612],{"class":418},[408,675,676],{"class":569},"\u002F* The real announcement: polite, off-screen, focus-preserving. *\u002F",[408,678,618],{"class":418},[408,680,682,684,686,689,691,694,697,699,702],{"class":410,"line":681},25,[408,683,624],{"class":418},[408,685,324],{"class":579},[408,687,688],{"class":461}," role",[408,690,586],{"class":432},[408,692,693],{"class":414},"\"status\"",[408,695,696],{"class":461}," className",[408,698,586],{"class":432},[408,700,701],{"class":414},"\"sr-only\"",[408,703,704],{"class":418},">\n",[408,706,708],{"class":410,"line":707},26,[408,709,710],{"class":418},"            Loading content, please wait.\n",[408,712,714,717,719],{"class":410,"line":713},27,[408,715,716],{"class":418},"          \u003C\u002F",[408,718,324],{"class":579},[408,720,704],{"class":418},[408,722,724],{"class":410,"line":723},28,[408,725,726],{"class":418},"        \u003C\u002F>\n",[408,728,730,733,735],{"class":410,"line":729},29,[408,731,732],{"class":418},"      ) ",[408,734,492],{"class":432},[408,736,563],{"class":418},[408,738,740],{"class":410,"line":739},30,[408,741,742],{"class":418},"        children\n",[408,744,746],{"class":410,"line":745},31,[408,747,748],{"class":418},"      )}\n",[408,750,752,755,757],{"class":410,"line":751},32,[408,753,754],{"class":418},"    \u003C\u002F",[408,756,580],{"class":579},[408,758,704],{"class":418},[408,760,762],{"class":410,"line":761},33,[408,763,764],{"class":418},"  );\n",[408,766,768],{"class":410,"line":767},34,[408,769,618],{"class":418},[324,771,772,773,776],{},"Keep the announcement concise and singular. A live region that re-renders \"Loading…\" on every keystroke or poll floods the speech queue; announce the state once when it begins and once when it resolves. The companion guide ",[337,774,133],{"href":775},"\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002F"," covers the broader discipline of keeping live regions quiet enough to be useful.",[324,778,779,780,782],{},"The ",[356,781,375],{}," class is the standard visually-hidden recipe—present in the DOM and the accessibility tree, but clipped out of view:",[399,784,788],{"className":785,"code":786,"language":787,"meta":404,"style":404},"language-css shiki shiki-themes github-light github-dark",".sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n  border: 0;\n}\n","css",[356,789,790,797,810,825,838,850,864,876,901,913,924],{"__ignoreMap":404},[408,791,792,795],{"class":410,"line":411},[408,793,794],{"class":461},".sr-only",[408,796,495],{"class":418},[408,798,799,802,805,808],{"class":410,"line":422},[408,800,801],{"class":505},"  position",[408,803,804],{"class":418},": ",[408,806,807],{"class":505},"absolute",[408,809,419],{"class":418},[408,811,812,815,817,820,823],{"class":410,"line":429},[408,813,814],{"class":505},"  width",[408,816,804],{"class":418},[408,818,819],{"class":505},"1",[408,821,822],{"class":432},"px",[408,824,419],{"class":418},[408,826,827,830,832,834,836],{"class":410,"line":447},[408,828,829],{"class":505},"  height",[408,831,804],{"class":418},[408,833,819],{"class":505},[408,835,822],{"class":432},[408,837,419],{"class":418},[408,839,840,843,845,848],{"class":410,"line":452},[408,841,842],{"class":505},"  padding",[408,844,804],{"class":418},[408,846,847],{"class":505},"0",[408,849,419],{"class":418},[408,851,852,855,857,860,862],{"class":410,"line":468},[408,853,854],{"class":505},"  margin",[408,856,804],{"class":418},[408,858,859],{"class":505},"-1",[408,861,822],{"class":432},[408,863,419],{"class":418},[408,865,866,869,871,874],{"class":410,"line":478},[408,867,868],{"class":505},"  overflow",[408,870,804],{"class":418},[408,872,873],{"class":505},"hidden",[408,875,419],{"class":418},[408,877,878,881,883,886,889,891,894,896,898],{"class":410,"line":486},[408,879,880],{"class":505},"  clip",[408,882,804],{"class":418},[408,884,885],{"class":505},"rect",[408,887,888],{"class":418},"(",[408,890,847],{"class":505},[408,892,893],{"class":505}," 0",[408,895,893],{"class":505},[408,897,893],{"class":505},[408,899,900],{"class":418},");\n",[408,902,903,906,908,911],{"class":410,"line":498},[408,904,905],{"class":505},"  white-space",[408,907,804],{"class":418},[408,909,910],{"class":505},"nowrap",[408,912,419],{"class":418},[408,914,915,918,920,922],{"class":410,"line":511},[408,916,917],{"class":505},"  border",[408,919,804],{"class":418},[408,921,847],{"class":505},[408,923,419],{"class":418},[408,925,926],{"class":410,"line":528},[408,927,618],{"class":418},[342,929],{},[345,931,933],{"id":932},"marking-the-visual-indicator-decorative","Marking the Visual Indicator Decorative",[324,935,936,937,940,941,943],{},"The spinner or skeleton is pure decoration once the live region carries the meaning. Leaving it exposed causes a screen reader to either announce nothing useful (an unlabeled SVG) or, worse, to double-report alongside the live region. Hide it with ",[356,938,939],{},"aria-hidden=\"true\""," so only the ",[356,942,362],{}," text reaches assistive technology.",[324,945,946,947,951,952,955,956,959],{},"This division of labor is the whole pattern: ",[948,949,950],"strong",{},"one element speaks, the other is seen."," Never label the spinner itself with ",[356,953,954],{},"aria-label=\"Loading\""," ",[328,957,958],{},"and"," also render a status region—that produces two announcements for one event. Pick the live region; it is the one that respects politeness and does not steal focus.",[399,961,965],{"className":962,"code":963,"language":964,"meta":404,"style":404},"language-html shiki shiki-themes github-light github-dark","\u003C!-- Decorative: seen, never announced -->\n\u003Csvg class=\"spinner\" aria-hidden=\"true\" viewBox=\"0 0 24 24\" focusable=\"false\">\n  \u003Ccircle cx=\"12\" cy=\"12\" r=\"10\" \u002F>\n\u003C\u002Fsvg>\n\n\u003C!-- Announced: heard, never seen -->\n\u003Cp role=\"status\" class=\"sr-only\">Loading results\u003C\u002Fp>\n","html",[356,966,967,972,1014,1048,1057,1061,1066],{"__ignoreMap":404},[408,968,969],{"class":410,"line":411},[408,970,971],{"class":569},"\u003C!-- Decorative: seen, never announced -->\n",[408,973,974,977,980,983,985,988,991,993,996,999,1001,1004,1007,1009,1012],{"class":410,"line":422},[408,975,976],{"class":418},"\u003C",[408,978,979],{"class":579},"svg",[408,981,982],{"class":461}," class",[408,984,586],{"class":418},[408,986,987],{"class":414},"\"spinner\"",[408,989,990],{"class":461}," aria-hidden",[408,992,586],{"class":418},[408,994,995],{"class":414},"\"true\"",[408,997,998],{"class":461}," viewBox",[408,1000,586],{"class":418},[408,1002,1003],{"class":414},"\"0 0 24 24\"",[408,1005,1006],{"class":461}," focusable",[408,1008,586],{"class":418},[408,1010,1011],{"class":414},"\"false\"",[408,1013,704],{"class":418},[408,1015,1016,1019,1022,1025,1027,1030,1033,1035,1037,1040,1042,1045],{"class":410,"line":429},[408,1017,1018],{"class":418},"  \u003C",[408,1020,1021],{"class":579},"circle",[408,1023,1024],{"class":461}," cx",[408,1026,586],{"class":418},[408,1028,1029],{"class":414},"\"12\"",[408,1031,1032],{"class":461}," cy",[408,1034,586],{"class":418},[408,1036,1029],{"class":414},[408,1038,1039],{"class":461}," r",[408,1041,586],{"class":418},[408,1043,1044],{"class":414},"\"10\"",[408,1046,1047],{"class":418}," \u002F>\n",[408,1049,1050,1053,1055],{"class":410,"line":447},[408,1051,1052],{"class":418},"\u003C\u002F",[408,1054,979],{"class":579},[408,1056,704],{"class":418},[408,1058,1059],{"class":410,"line":452},[408,1060,426],{"emptyLinePlaceholder":425},[408,1062,1063],{"class":410,"line":468},[408,1064,1065],{"class":569},"\u003C!-- Announced: heard, never seen -->\n",[408,1067,1068,1070,1072,1074,1076,1078,1080,1082,1084,1087,1089],{"class":410,"line":478},[408,1069,976],{"class":418},[408,1071,324],{"class":579},[408,1073,688],{"class":461},[408,1075,586],{"class":418},[408,1077,693],{"class":414},[408,1079,982],{"class":461},[408,1081,586],{"class":418},[408,1083,701],{"class":414},[408,1085,1086],{"class":418},">Loading results\u003C\u002F",[408,1088,324],{"class":579},[408,1090,704],{"class":418},[324,1092,1093,1094,1097],{},"Adding ",[356,1095,1096],{},"focusable=\"false\""," to inline SVG keeps legacy versions of Internet Explorer and some screen-reader\u002Fbrowser pairings from placing the decorative graphic in the tab order—a small but worthwhile defense.",[342,1099],{},[345,1101,1103],{"id":1102},"stopping-shimmer-under-reduced-motion","Stopping Shimmer Under Reduced Motion",[324,1105,1106,1107,1110,1111,1114,1115,1118],{},"Skeleton shimmer is a gradient sweeping horizontally across placeholder blocks—spatial, looping motion that falls squarely under ",[356,1108,1109],{},"2.3.3 Animation from Interactions"," and the spirit of ",[356,1112,1113],{},"2.2.2 Pause, Stop, Hide",". Under ",[356,1116,1117],{},"prefers-reduced-motion: reduce"," it must stop. The accessible substitute is a static muted fill or, at most, a gentle in-place opacity pulse that moves nothing through space.",[324,1120,1121,1122,1125],{},"Following the motion-safe default, the shimmer animation lives only inside a ",[356,1123,1124],{},"no-preference"," query, so reduced-motion users get the static skeleton automatically:",[399,1127,1129],{"className":785,"code":1128,"language":787,"meta":404,"style":404},".skeleton {\n  background: var(--surface);\n  border-radius: 6px;\n}\n\n\u002F* Sweep only when the user has not asked to reduce motion. *\u002F\n@media (prefers-reduced-motion: no-preference) {\n  .skeleton {\n    background: linear-gradient(\n      90deg,\n      var(--surface) 25%,\n      var(--primary-soft) 50%,\n      var(--surface) 75%\n    );\n    background-size: 200% 100%;\n    animation: skeleton-sweep 1.4s ease-in-out infinite;\n  }\n}\n\n@keyframes skeleton-sweep {\n  to { background-position: -200% 0; }\n}\n\n\u002F* The spinner: rotation is acceptable feedback, but slow it and avoid *\u002F\n\u002F* large sweeps under reduce. *\u002F\n.spinner { animation: spin 0.8s linear infinite; }\n.spinner--static { animation: none; opacity: 0.7; } \u002F* JS-gated reduced variant *\u002F\n\n@keyframes spin {\n  to { transform: rotate(360deg); }\n}\n",[356,1130,1131,1138,1155,1169,1173,1177,1182,1190,1197,1210,1220,1240,1258,1274,1279,1298,1320,1325,1329,1333,1343,1366,1370,1374,1379,1384,1409,1440,1444,1453,1477],{"__ignoreMap":404},[408,1132,1133,1136],{"class":410,"line":411},[408,1134,1135],{"class":461},".skeleton",[408,1137,495],{"class":418},[408,1139,1140,1143,1145,1148,1150,1153],{"class":410,"line":422},[408,1141,1142],{"class":505},"  background",[408,1144,804],{"class":418},[408,1146,1147],{"class":505},"var",[408,1149,888],{"class":418},[408,1151,1152],{"class":471},"--surface",[408,1154,900],{"class":418},[408,1156,1157,1160,1162,1165,1167],{"class":410,"line":429},[408,1158,1159],{"class":505},"  border-radius",[408,1161,804],{"class":418},[408,1163,1164],{"class":505},"6",[408,1166,822],{"class":432},[408,1168,419],{"class":418},[408,1170,1171],{"class":410,"line":447},[408,1172,618],{"class":418},[408,1174,1175],{"class":410,"line":452},[408,1176,426],{"emptyLinePlaceholder":425},[408,1178,1179],{"class":410,"line":468},[408,1180,1181],{"class":569},"\u002F* Sweep only when the user has not asked to reduce motion. *\u002F\n",[408,1183,1184,1187],{"class":410,"line":478},[408,1185,1186],{"class":432},"@media",[408,1188,1189],{"class":418}," (prefers-reduced-motion: no-preference) {\n",[408,1191,1192,1195],{"class":410,"line":486},[408,1193,1194],{"class":461},"  .skeleton",[408,1196,495],{"class":418},[408,1198,1199,1202,1204,1207],{"class":410,"line":498},[408,1200,1201],{"class":505},"    background",[408,1203,804],{"class":418},[408,1205,1206],{"class":505},"linear-gradient",[408,1208,1209],{"class":418},"(\n",[408,1211,1212,1215,1218],{"class":410,"line":511},[408,1213,1214],{"class":505},"      90",[408,1216,1217],{"class":432},"deg",[408,1219,475],{"class":418},[408,1221,1222,1225,1227,1229,1232,1235,1238],{"class":410,"line":528},[408,1223,1224],{"class":505},"      var",[408,1226,888],{"class":418},[408,1228,1152],{"class":471},[408,1230,1231],{"class":418},") ",[408,1233,1234],{"class":505},"25",[408,1236,1237],{"class":432},"%",[408,1239,475],{"class":418},[408,1241,1242,1244,1246,1249,1251,1254,1256],{"class":410,"line":534},[408,1243,1224],{"class":505},[408,1245,888],{"class":418},[408,1247,1248],{"class":471},"--primary-soft",[408,1250,1231],{"class":418},[408,1252,1253],{"class":505},"50",[408,1255,1237],{"class":432},[408,1257,475],{"class":418},[408,1259,1260,1262,1264,1266,1268,1271],{"class":410,"line":552},[408,1261,1224],{"class":505},[408,1263,888],{"class":418},[408,1265,1152],{"class":471},[408,1267,1231],{"class":418},[408,1269,1270],{"class":505},"75",[408,1272,1273],{"class":432},"%\n",[408,1275,1276],{"class":410,"line":557},[408,1277,1278],{"class":418},"    );\n",[408,1280,1281,1284,1286,1289,1291,1294,1296],{"class":410,"line":566},[408,1282,1283],{"class":505},"    background-size",[408,1285,804],{"class":418},[408,1287,1288],{"class":505},"200",[408,1290,1237],{"class":432},[408,1292,1293],{"class":505}," 100",[408,1295,1237],{"class":432},[408,1297,419],{"class":418},[408,1299,1300,1303,1306,1309,1312,1315,1318],{"class":410,"line":573},[408,1301,1302],{"class":505},"    animation",[408,1304,1305],{"class":418},": skeleton-sweep ",[408,1307,1308],{"class":505},"1.4",[408,1310,1311],{"class":432},"s",[408,1313,1314],{"class":505}," ease-in-out",[408,1316,1317],{"class":505}," infinite",[408,1319,419],{"class":418},[408,1321,1322],{"class":410,"line":592},[408,1323,1324],{"class":418},"  }\n",[408,1326,1327],{"class":410,"line":603},[408,1328,618],{"class":418},[408,1330,1331],{"class":410,"line":609},[408,1332,426],{"emptyLinePlaceholder":425},[408,1334,1335,1338,1341],{"class":410,"line":621},[408,1336,1337],{"class":432},"@keyframes",[408,1339,1340],{"class":471}," skeleton-sweep",[408,1342,495],{"class":418},[408,1344,1345,1348,1351,1354,1356,1359,1361,1363],{"class":410,"line":630},[408,1346,1347],{"class":461},"  to",[408,1349,1350],{"class":418}," { ",[408,1352,1353],{"class":505},"background-position",[408,1355,804],{"class":418},[408,1357,1358],{"class":505},"-200",[408,1360,1237],{"class":432},[408,1362,893],{"class":505},[408,1364,1365],{"class":418},"; }\n",[408,1367,1368],{"class":410,"line":654},[408,1369,618],{"class":418},[408,1371,1372],{"class":410,"line":665},[408,1373,426],{"emptyLinePlaceholder":425},[408,1375,1376],{"class":410,"line":671},[408,1377,1378],{"class":569},"\u002F* The spinner: rotation is acceptable feedback, but slow it and avoid *\u002F\n",[408,1380,1381],{"class":410,"line":681},[408,1382,1383],{"class":569},"\u002F* large sweeps under reduce. *\u002F\n",[408,1385,1386,1389,1391,1394,1397,1400,1402,1405,1407],{"class":410,"line":707},[408,1387,1388],{"class":461},".spinner",[408,1390,1350],{"class":418},[408,1392,1393],{"class":505},"animation",[408,1395,1396],{"class":418},": spin ",[408,1398,1399],{"class":505},"0.8",[408,1401,1311],{"class":432},[408,1403,1404],{"class":505}," linear",[408,1406,1317],{"class":505},[408,1408,1365],{"class":418},[408,1410,1411,1414,1416,1418,1420,1423,1426,1429,1431,1434,1437],{"class":410,"line":713},[408,1412,1413],{"class":461},".spinner--static",[408,1415,1350],{"class":418},[408,1417,1393],{"class":505},[408,1419,804],{"class":418},[408,1421,1422],{"class":505},"none",[408,1424,1425],{"class":418},"; ",[408,1427,1428],{"class":505},"opacity",[408,1430,804],{"class":418},[408,1432,1433],{"class":505},"0.7",[408,1435,1436],{"class":418},"; } ",[408,1438,1439],{"class":569},"\u002F* JS-gated reduced variant *\u002F\n",[408,1441,1442],{"class":410,"line":723},[408,1443,426],{"emptyLinePlaceholder":425},[408,1445,1446,1448,1451],{"class":410,"line":729},[408,1447,1337],{"class":432},[408,1449,1450],{"class":471}," spin",[408,1452,495],{"class":418},[408,1454,1455,1457,1459,1462,1464,1467,1469,1472,1474],{"class":410,"line":739},[408,1456,1347],{"class":461},[408,1458,1350],{"class":418},[408,1460,1461],{"class":505},"transform",[408,1463,804],{"class":418},[408,1465,1466],{"class":505},"rotate",[408,1468,888],{"class":418},[408,1470,1471],{"class":505},"360",[408,1473,1217],{"class":432},[408,1475,1476],{"class":418},"); }\n",[408,1478,1479],{"class":410,"line":745},[408,1480,618],{"class":418},[324,1482,1483,1484,1487,1488,1490,1491,1493],{},"A spinner's rotation is closer to ",[328,1485,1486],{},"essential"," feedback than a shimmer sweep, but it is still motion. Under reduced motion, prefer slowing it dramatically or swapping to a non-rotating \"working\" indicator (a pulsing dot, a determinate progress bar) over a fast continuous spin. The ",[356,1489,392],{}," plus ",[356,1492,362],{}," announcement already conveys the state, so a reduced or static visual loses nothing semantically.",[342,1495],{},[345,1497,1499],{"id":1498},"not-trapping-focus-while-loading","Not Trapping Focus While Loading",[324,1501,1502,1503,1506,1507,1510,1511,1514],{},"A loading state is transient, and focus must survive it. Two failure modes are common. First, ",[948,1504,1505],{},"moving focus to the spinner",": calling ",[356,1508,1509],{},".focus()"," on a loading indicator yanks the keyboard user out of context, and when loading finishes the indicator unmounts, leaving focus on a detached node. Never focus a decorative, soon-to-disappear element. Second, ",[948,1512,1513],{},"trapping focus"," inside the loading region as if it were a modal—loading is not a modal interaction and the user must remain free to navigate or cancel.",[324,1516,1517,1518,1520,1521,1523,1524,340],{},"Let focus rest where the user left it. The ",[356,1519,362],{}," region announces progress without moving the cursor, which is precisely why ",[356,1522,384],{}," is the right tool—it informs without hijacking. When content finishes loading, decide focus deliberately: if the load was a route change or a user-requested result set, move focus to the new content's heading; if it was a background refresh, leave focus untouched. This coordinates with broader focus discipline in ",[337,1525,25],{"href":1526},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002F",[342,1528],{},[345,1530,1532],{"id":1531},"how-to-verify","How to Verify",[324,1534,1535,1538,1539,1541],{},[948,1536,1537],{},"1. Screen reader announcement."," With NVDA (Firefox), VoiceOver (Safari), or Narrator, trigger the loading state. You should hear the ",[356,1540,362],{}," text (\"Loading content…\") announced once, politely, without focus moving. When data arrives, confirm the busy state clears and the spinner text is gone—no lingering \"Loading\" in the buffer.",[324,1543,1544,1547,1548,1550,1551,1553,1554,1556],{},[948,1545,1546],{},"2. Accessibility tree inspection."," In Chrome DevTools, open the Accessibility pane and select the loading region. Confirm the spinner\u002FSVG is absent from the tree (because of ",[356,1549,939],{},") and the status node is present with role \"status\" and ",[356,1552,384],{},". Verify ",[356,1555,388],{}," on the container while loading.",[324,1558,1559,1562,1563,393,1566,1569,1570,1573],{},[948,1560,1561],{},"3. Reduced-motion check."," In DevTools → Rendering, set ",[948,1564,1565],{},"Emulate CSS media feature prefers-reduced-motion",[356,1567,1568],{},"reduce",", then trigger loading. The shimmer sweep must stop—the skeleton should show a flat fill—and the spinner should fall back to its static or slowed variant. Repeat with the real OS \"Reduce motion\" setting to exercise the ",[356,1571,1572],{},"useReducedMotion"," change listener.",[324,1575,1576,1579],{},[948,1577,1578],{},"4. Focus check."," Tab to a control, trigger a load, and confirm focus stays on that control throughout—it must not jump to the spinner. After loading completes, verify focus lands where your design intends (new content heading for a user-requested load, unchanged for a background refresh).",[324,1581,1582,1585],{},[948,1583,1584],{},"5. Automated assertion."," With Testing Library, assert the contract:",[399,1587,1589],{"className":401,"code":1588,"language":403,"meta":404,"style":404},"import { render, screen } from '@testing-library\u002Freact';\nimport { LoadingPanel } from '.\u002FLoadingPanel';\n\ntest('exposes loading state to assistive tech', () => {\n  render(\u003CLoadingPanel loading>{\u003Cp>Data\u003C\u002Fp>}\u003C\u002FLoadingPanel>);\n\n  const status = screen.getByRole('status');\n  expect(status).toHaveTextContent(\u002Floading\u002Fi);\n  expect(status).toHaveAttribute('aria-live', 'polite');\n  \u002F\u002F The decorative spinner is hidden, so it is not an accessible element.\n  expect(screen.queryByRole('img')).toBeNull();\n});\n",[356,1590,1591,1605,1619,1623,1641,1673,1677,1699,1725,1747,1752,1775],{"__ignoreMap":404},[408,1592,1593,1595,1598,1600,1603],{"class":410,"line":411},[408,1594,433],{"class":432},[408,1596,1597],{"class":418}," { render, screen } ",[408,1599,439],{"class":432},[408,1601,1602],{"class":414}," '@testing-library\u002Freact'",[408,1604,419],{"class":418},[408,1606,1607,1609,1612,1614,1617],{"class":410,"line":422},[408,1608,433],{"class":432},[408,1610,1611],{"class":418}," { LoadingPanel } ",[408,1613,439],{"class":432},[408,1615,1616],{"class":414}," '.\u002FLoadingPanel'",[408,1618,419],{"class":418},[408,1620,1621],{"class":410,"line":429},[408,1622,426],{"emptyLinePlaceholder":425},[408,1624,1625,1628,1630,1633,1636,1639],{"class":410,"line":447},[408,1626,1627],{"class":461},"test",[408,1629,888],{"class":418},[408,1631,1632],{"class":414},"'exposes loading state to assistive tech'",[408,1634,1635],{"class":418},", () ",[408,1637,1638],{"class":432},"=>",[408,1640,495],{"class":418},[408,1642,1643,1646,1649,1652,1655,1658,1660,1663,1665,1668,1670],{"class":410,"line":452},[408,1644,1645],{"class":461},"  render",[408,1647,1648],{"class":418},"(\u003C",[408,1650,1651],{"class":505},"LoadingPanel",[408,1653,1654],{"class":461}," loading",[408,1656,1657],{"class":418},">{\u003C",[408,1659,324],{"class":579},[408,1661,1662],{"class":418},">Data\u003C\u002F",[408,1664,324],{"class":579},[408,1666,1667],{"class":418},">}\u003C\u002F",[408,1669,1651],{"class":505},[408,1671,1672],{"class":418},">);\n",[408,1674,1675],{"class":410,"line":468},[408,1676,426],{"emptyLinePlaceholder":425},[408,1678,1679,1681,1684,1686,1689,1692,1694,1697],{"class":410,"line":478},[408,1680,537],{"class":432},[408,1682,1683],{"class":505}," status",[408,1685,543],{"class":432},[408,1687,1688],{"class":418}," screen.",[408,1690,1691],{"class":461},"getByRole",[408,1693,888],{"class":418},[408,1695,1696],{"class":414},"'status'",[408,1698,900],{"class":418},[408,1700,1701,1704,1707,1710,1712,1714,1718,1720,1723],{"class":410,"line":486},[408,1702,1703],{"class":461},"  expect",[408,1705,1706],{"class":418},"(status).",[408,1708,1709],{"class":461},"toHaveTextContent",[408,1711,888],{"class":418},[408,1713,86],{"class":414},[408,1715,1717],{"class":1716},"sA_wV","loading",[408,1719,86],{"class":414},[408,1721,1722],{"class":432},"i",[408,1724,900],{"class":418},[408,1726,1727,1729,1731,1734,1736,1739,1742,1745],{"class":410,"line":498},[408,1728,1703],{"class":461},[408,1730,1706],{"class":418},[408,1732,1733],{"class":461},"toHaveAttribute",[408,1735,888],{"class":418},[408,1737,1738],{"class":414},"'aria-live'",[408,1740,1741],{"class":418},", ",[408,1743,1744],{"class":414},"'polite'",[408,1746,900],{"class":418},[408,1748,1749],{"class":410,"line":511},[408,1750,1751],{"class":569},"  \u002F\u002F The decorative spinner is hidden, so it is not an accessible element.\n",[408,1753,1754,1756,1759,1762,1764,1767,1770,1773],{"class":410,"line":528},[408,1755,1703],{"class":461},[408,1757,1758],{"class":418},"(screen.",[408,1760,1761],{"class":461},"queryByRole",[408,1763,888],{"class":418},[408,1765,1766],{"class":414},"'img'",[408,1768,1769],{"class":418},")).",[408,1771,1772],{"class":461},"toBeNull",[408,1774,549],{"class":418},[408,1776,1777],{"class":410,"line":534},[408,1778,1779],{"class":418},"});\n",[342,1781],{},[345,1783,1785],{"id":1784},"common-a11y-mistakes","Common a11y Mistakes",[1787,1788,1789,1799,1810,1819,1835,1841],"ul",{},[1790,1791,1792,1795,1796,1798],"li",{},[948,1793,1794],{},"Silent spinner:"," A rotating SVG with no ",[356,1797,362],{}," region tells screen-reader users nothing. Always pair decoration with an announcement.",[1790,1800,1801,1804,1805,955,1807,1809],{},[948,1802,1803],{},"Double announcement:"," Labeling the spinner with ",[356,1806,954],{},[328,1808,958],{}," rendering a live region reports the same event twice. Use only the live region.",[1790,1811,1812,1815,1816,1818],{},[948,1813,1814],{},"Exposed decoration:"," Forgetting ",[356,1817,939],{}," on the spinner lets it clutter the accessibility tree. Hide it.",[1790,1820,1821,1824,1825,86,1828,1831,1832,1834],{},[948,1822,1823],{},"Shimmer that ignores reduced motion:"," A looping gradient sweep violates the spirit of ",[356,1826,1827],{},"2.2.2",[356,1829,1830],{},"2.3.3",". Gate it behind ",[356,1833,1124],{}," and fall back to a static fill.",[1790,1836,1837,1840],{},[948,1838,1839],{},"Focusing the indicator:"," Moving focus to a spinner that will unmount leaves focus on a detached node. Leave focus in place.",[1790,1842,1843,1846],{},[948,1844,1845],{},"Chatty live region:"," Re-announcing \"Loading\" on every poll floods the speech queue. Announce once at start, once at finish.",[342,1848],{},[345,1850,1852],{"id":1851},"frequently-asked-questions","Frequently Asked Questions",[324,1854,1855,1865,1866,1868,1869,1871],{},[948,1856,1857,1858,1860,1861,1864],{},"Should I use ",[356,1859,362],{}," or ",[356,1862,1863],{},"role=\"alert\""," for a loading indicator?","\nUse ",[356,1867,362],{},", which is polite and queues the announcement without interrupting. ",[356,1870,1863],{}," is assertive and interrupts the user immediately—appropriate for errors, not for routine loading. For loading you want to inform, not interrupt.",[324,1873,1874,1882,1883,1885,1886,1888,1889,1891,1892,1894],{},[948,1875,1876,1877,1879,1880,598],{},"Do I need both ",[356,1878,392],{}," and ",[356,1881,362],{},"\nThey do different jobs. ",[356,1884,388],{}," marks a region whose contents are mid-update so assistive technology knows not to trust them yet; ",[356,1887,362],{}," delivers the human-readable \"Loading\" announcement. Use ",[356,1890,392],{}," on the container and ",[356,1893,362],{}," on a concise text node inside it.",[324,1896,1897,1900,1901,1904,1905,1908],{},[948,1898,1899],{},"How do I stop skeleton shimmer for motion-sensitive users?","\nDefine the shimmer animation only inside ",[356,1902,1903],{},"@media (prefers-reduced-motion: no-preference)"," so reduced-motion users automatically get a static skeleton. For JavaScript-driven variants, gate the animated class on a ",[356,1906,1907],{},"useReducedMotion()"," hook and swap to a flat or gently pulsing fill.",[324,1910,1911,1914],{},[948,1912,1913],{},"Should loading indicators move keyboard focus?","\nNo. Loading is transient and the indicator will unmount, so focusing it strands the keyboard user on a detached node. Let focus rest where it was; the polite live region announces progress without moving the cursor. Move focus only after loading completes, and only when the interaction warrants it.",[342,1916],{},[345,1918,1920],{"id":1919},"related-guides","Related guides",[1787,1922,1923,1927,1932],{},[1790,1924,1925],{},[337,1926,49],{"href":339},[1790,1928,1929],{},[337,1930,61],{"href":1931},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002F",[1790,1933,1934],{},[337,1935,133],{"href":775},[1937,1938,1939],"style",{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .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}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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":404,"searchDepth":422,"depth":422,"links":1941},[1942,1943,1944,1945,1946,1947,1948,1949,1950],{"id":347,"depth":422,"text":348},{"id":368,"depth":422,"text":369},{"id":932,"depth":422,"text":933},{"id":1102,"depth":422,"text":1103},{"id":1498,"depth":422,"text":1499},{"id":1531,"depth":422,"text":1532},{"id":1784,"depth":422,"text":1785},{"id":1851,"depth":422,"text":1852},{"id":1919,"depth":422,"text":1920},null,"Build loading indicators screen readers and motion-sensitive users can handle—role=status with aria-busy, sr-only text, and shimmer that stops under reduced motion.","md",{},false,{"title":55,"description":1952},"U3-iyGbii0Xz3-BT5DXfXYdUDxYf9ZQccMIFsBo5W8Y",[1959,1998,1999,2062],{"title":5,"path":6,"stem":7,"children":1960},[1961,1962,1965,1968,1974,1980,1989,1995],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1963},[1964],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1966},[1967],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1969},[1970,1971],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":1972},[1973],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":1975},[1976,1977],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":1978},[1979],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":1981},[1982,1983,1986],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":1984},[1985],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":1987},[1988],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":1990},[1991,1992],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":1993},[1994],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":1996},[1997],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2000},[2001,2002,2008,2020,2032,2035,2044,2056],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2003},[2004,2005],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2006},[2007],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2009},[2010,2011,2014,2017],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2012},[2013],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2015},[2016],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2018},[2019],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2021},[2022,2023,2026,2029],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2024},[2025],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2027},[2028],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2030},[2031],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2033},[2034],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2036},[2037,2038,2041],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2039},[2040],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2042},[2043],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2045},[2046,2047,2050,2053],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2048},[2049],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2051},[2052],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2054},[2055],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2057},[2058,2059],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2060},[2061],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2063},[2064,2065,2074,2083,2092,2101],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2066},[2067,2068,2071],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2069},[2070],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2072},[2073],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2075},[2076,2077,2080],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2078},[2079],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2081},[2082],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2084},[2085,2086,2089],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2087},[2088],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2090},[2091],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2093},[2094,2095,2098],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2096},[2097],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2099},[2100],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2102},[2103,2104,2107],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2105},[2106],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2108},[2109],{"title":309,"path":310,"stem":311},1781785523923]