[{"data":1,"prerenderedAt":2225},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002F":314,"content-navigation":2073},[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":61,"body":316,"date":2066,"description":2067,"extension":2068,"image":2066,"meta":2069,"modifiedAt":2066,"navigation":542,"noindex":2070,"path":62,"publishedAt":2066,"seo":2071,"stem":63,"updatedAt":2066,"__hash__":2072},"content\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Frespecting-prefers-reduced-motion-in-react-and-css\u002Findex.md",{"type":317,"value":318,"toc":2054},"minimark",[319,323,349,352,357,375,391,393,397,404,497,513,593,617,727,734,736,743,755,1064,1081,1088,1090,1094,1097,1377,1402,1405,1479,1481,1485,1495,1517,1530,1540,1542,1546,1549,1555,1584,1594,1618,1627,1885,1887,1891,1968,1970,1974,1989,2003,2020,2029,2031,2035,2050],[320,321,61],"h1",{"id":322},"respecting-prefers-reduced-motion-in-react-and-css",[324,325,326,327,331,332,335,336,339,340,343,344,348],"p",{},"A single application animates in two different runtimes. CSS transitions and keyframes are declarative and the browser owns them; ",[328,329,330],"code",{},"Framer Motion"," springs, GSAP timelines, and hand-rolled ",[328,333,334],{},"requestAnimationFrame"," loops run in JavaScript and the browser knows nothing about your intent. ",[328,337,338],{},"prefers-reduced-motion"," reaches the first layer for free through a media query, but the second layer needs an explicit read in code. This guide gives you both halves—a CSS-first pattern and a ",[328,341,342],{},"useReducedMotion()"," hook—so every animation in a framework app respects the user's choice. It is the implementation companion to ",[345,346,49],"a",{"href":347},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002F",".",[350,351],"hr",{},[353,354,356],"h2",{"id":355},"prerequisites","Prerequisites",[324,358,359,360,363,364,367,368,371,372,374],{},"This guide assumes you can already render a component and write a stylesheet in a React (or Next.js) project. You should be familiar with ",[328,361,362],{},"useEffect"," and ",[328,365,366],{},"useState",", and—if you use one—the basics of your animation library. No external dependency is required for the core pattern; ",[328,369,370],{},"window.matchMedia"," is a built-in browser API. ",[328,373,330],{}," examples are optional and clearly marked.",[324,376,377,378,381,382,385,386,390],{},"One conceptual prerequisite: understand the two states of the media feature. ",[328,379,380],{},"(prefers-reduced-motion: reduce)"," matches when the user has asked their OS to minimize motion; ",[328,383,384],{},"(prefers-reduced-motion: no-preference)"," matches when they have not. There is no third state, and the absence of a preference is ",[387,388,389],"em",{},"not"," the same as a preference for motion—it simply means you have not been told to reduce.",[350,392],{},[353,394,396],{"id":395},"the-css-first-pattern","The CSS-First Pattern",[324,398,399,400,403],{},"CSS should carry as much of the work as possible because the browser applies it before any JavaScript runs, with zero hydration risk. The defensive instinct is to define an animation and strip it under ",[328,401,402],{},"reduce",":",[405,406,411],"pre",{"className":407,"code":408,"language":409,"meta":410,"style":410},"language-css shiki shiki-themes github-light github-dark","\u002F* Reactive: easy to forget the override on the next animation you add *\u002F\n.panel {\n  transition: transform 220ms ease;\n}\n@media (prefers-reduced-motion: reduce) {\n  .panel { transition: none; }\n}\n","css","",[328,412,413,422,433,456,462,471,492],{"__ignoreMap":410},[414,415,418],"span",{"class":416,"line":417},"line",1,[414,419,421],{"class":420},"sJ8bj","\u002F* Reactive: easy to forget the override on the next animation you add *\u002F\n",[414,423,425,429],{"class":416,"line":424},2,[414,426,428],{"class":427},"sScJk",".panel",[414,430,432],{"class":431},"sVt8B"," {\n",[414,434,436,440,443,446,450,453],{"class":416,"line":435},3,[414,437,439],{"class":438},"sj4cs","  transition",[414,441,442],{"class":431},": transform ",[414,444,445],{"class":438},"220",[414,447,449],{"class":448},"szBVR","ms",[414,451,452],{"class":438}," ease",[414,454,455],{"class":431},";\n",[414,457,459],{"class":416,"line":458},4,[414,460,461],{"class":431},"}\n",[414,463,465,468],{"class":416,"line":464},5,[414,466,467],{"class":448},"@media",[414,469,470],{"class":431}," (prefers-reduced-motion: reduce) {\n",[414,472,474,477,480,483,486,489],{"class":416,"line":473},6,[414,475,476],{"class":427},"  .panel",[414,478,479],{"class":431}," { ",[414,481,482],{"class":438},"transition",[414,484,485],{"class":431},": ",[414,487,488],{"class":438},"none",[414,490,491],{"class":431},"; }\n",[414,493,495],{"class":416,"line":494},7,[414,496,461],{"class":431},[324,498,499,500,504,505,508,509,512],{},"The safer inversion is ",[501,502,503],"strong",{},"motion-safe",": put the animation ",[387,506,507],{},"inside"," a ",[328,510,511],{},"no-preference"," query so it only exists for users who have not asked to reduce. Forgetting the query then fails safe—you ship a static element, never an unwanted one.",[405,514,516],{"className":407,"code":515,"language":409,"meta":410,"style":410},"\u002F* Motion-safe: animation is opt-in, the static state is the default *\u002F\n.panel {\n  \u002F* resting, static appearance — no transition declared here *\u002F\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .panel {\n    transition: transform 220ms ease, opacity 220ms ease;\n  }\n}\n",[328,517,518,523,529,534,538,544,551,557,582,588],{"__ignoreMap":410},[414,519,520],{"class":416,"line":417},[414,521,522],{"class":420},"\u002F* Motion-safe: animation is opt-in, the static state is the default *\u002F\n",[414,524,525,527],{"class":416,"line":424},[414,526,428],{"class":427},[414,528,432],{"class":431},[414,530,531],{"class":416,"line":435},[414,532,533],{"class":420},"  \u002F* resting, static appearance — no transition declared here *\u002F\n",[414,535,536],{"class":416,"line":458},[414,537,461],{"class":431},[414,539,540],{"class":416,"line":464},[414,541,543],{"emptyLinePlaceholder":542},true,"\n",[414,545,546,548],{"class":416,"line":473},[414,547,467],{"class":448},[414,549,550],{"class":431}," (prefers-reduced-motion: no-preference) {\n",[414,552,553,555],{"class":416,"line":494},[414,554,476],{"class":427},[414,556,432],{"class":431},[414,558,560,563,565,567,569,571,574,576,578,580],{"class":416,"line":559},8,[414,561,562],{"class":438},"    transition",[414,564,442],{"class":431},[414,566,445],{"class":438},[414,568,449],{"class":448},[414,570,452],{"class":438},[414,572,573],{"class":431},", opacity ",[414,575,445],{"class":438},[414,577,449],{"class":448},[414,579,452],{"class":438},[414,581,455],{"class":431},[414,583,585],{"class":416,"line":584},9,[414,586,587],{"class":431},"  }\n",[414,589,591],{"class":416,"line":590},10,[414,592,461],{"class":431},[324,594,595,596,363,599,602,603,605,606,608,609,612,613,616],{},"If you use Tailwind, the ",[328,597,598],{},"motion-safe:",[328,600,601],{},"motion-reduce:"," variants compile to exactly these queries—prefer ",[328,604,598],{}," so the un-prefixed utility stays static. For a large existing codebase that cannot be refactored rule by rule, add a conservative global guard as a floor. Use a near-zero duration rather than ",[328,607,488],{}," so JavaScript waiting on ",[328,610,611],{},"transitionend"," or ",[328,614,615],{},"animationend"," still fires its callback:",[405,618,620],{"className":407,"code":619,"language":409,"meta":410,"style":410},"@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important; \u002F* break infinite loops *\u002F\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}\n",[328,621,622,628,637,646,655,672,690,705,719,723],{"__ignoreMap":410},[414,623,624,626],{"class":416,"line":417},[414,625,467],{"class":448},[414,627,470],{"class":431},[414,629,630,634],{"class":416,"line":424},[414,631,633],{"class":632},"s9eBZ","  *",[414,635,636],{"class":431},",\n",[414,638,639,641,644],{"class":416,"line":435},[414,640,633],{"class":632},[414,642,643],{"class":427},"::before",[414,645,636],{"class":431},[414,647,648,650,653],{"class":416,"line":458},[414,649,633],{"class":632},[414,651,652],{"class":427},"::after",[414,654,432],{"class":431},[414,656,657,660,662,665,667,670],{"class":416,"line":464},[414,658,659],{"class":438},"    animation-duration",[414,661,485],{"class":431},[414,663,664],{"class":438},"0.01",[414,666,449],{"class":448},[414,668,669],{"class":448}," !important",[414,671,455],{"class":431},[414,673,674,677,679,682,684,687],{"class":416,"line":473},[414,675,676],{"class":438},"    animation-iteration-count",[414,678,485],{"class":431},[414,680,681],{"class":438},"1",[414,683,669],{"class":448},[414,685,686],{"class":431},"; ",[414,688,689],{"class":420},"\u002F* break infinite loops *\u002F\n",[414,691,692,695,697,699,701,703],{"class":416,"line":494},[414,693,694],{"class":438},"    transition-duration",[414,696,485],{"class":431},[414,698,664],{"class":438},[414,700,449],{"class":448},[414,702,669],{"class":448},[414,704,455],{"class":431},[414,706,707,710,712,715,717],{"class":416,"line":559},[414,708,709],{"class":438},"    scroll-behavior",[414,711,485],{"class":431},[414,713,714],{"class":438},"auto",[414,716,669],{"class":448},[414,718,455],{"class":431},[414,720,721],{"class":416,"line":584},[414,722,587],{"class":431},[414,724,725],{"class":416,"line":590},[414,726,461],{"class":431},[324,728,729,730,733],{},"Treat the guard as a safety net while you migrate hot paths to the motion-safe pattern, not as the finished solution—it disables ",[387,731,732],{},"useful"," feedback along with the harmful motion.",[350,735],{},[353,737,739,740,742],{"id":738},"a-usereducedmotion-hook","A ",[328,741,342],{}," Hook",[324,744,745,746,748,749,751,752,754],{},"CSS cannot reach JavaScript-driven animation. To gate a ",[328,747,330],{}," spring, a GSAP timeline, or a ",[328,750,334],{}," loop, read the same media feature in code through ",[328,753,370],{}," and subscribe to changes.",[405,756,760],{"className":757,"code":758,"language":759,"meta":410,"style":410},"language-tsx shiki shiki-themes github-light github-dark","'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst QUERY = '(prefers-reduced-motion: reduce)';\n\n\u002F**\n * Returns true when the user prefers reduced motion.\n * Defaults to `true` so SSR and first paint assume the safe, low-motion\n * state — a vestibular user never sees a large animation before hydration.\n *\u002F\nexport function useReducedMotion(): boolean {\n  const [reduced, setReduced] = useState(true);\n\n  useEffect(() => {\n    const mql = window.matchMedia(QUERY); \u002F\u002F window only exists client-side\n    setReduced(mql.matches);              \u002F\u002F sync to the real value after mount\n\n    const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);\n    mql.addEventListener('change', onChange); \u002F\u002F respect a mid-session OS toggle\n    return () => mql.removeEventListener('change', onChange);\n  }, []);\n\n  return reduced;\n}\n","tsx",[328,761,762,770,774,790,794,810,814,819,824,829,834,840,862,898,903,917,945,957,962,995,1015,1039,1045,1050,1059],{"__ignoreMap":410},[414,763,764,768],{"class":416,"line":417},[414,765,767],{"class":766},"sZZnC","'use client'",[414,769,455],{"class":431},[414,771,772],{"class":416,"line":424},[414,773,543],{"emptyLinePlaceholder":542},[414,775,776,779,782,785,788],{"class":416,"line":435},[414,777,778],{"class":448},"import",[414,780,781],{"class":431}," { useEffect, useState } ",[414,783,784],{"class":448},"from",[414,786,787],{"class":766}," 'react'",[414,789,455],{"class":431},[414,791,792],{"class":416,"line":458},[414,793,543],{"emptyLinePlaceholder":542},[414,795,796,799,802,805,808],{"class":416,"line":464},[414,797,798],{"class":448},"const",[414,800,801],{"class":438}," QUERY",[414,803,804],{"class":448}," =",[414,806,807],{"class":766}," '(prefers-reduced-motion: reduce)'",[414,809,455],{"class":431},[414,811,812],{"class":416,"line":473},[414,813,543],{"emptyLinePlaceholder":542},[414,815,816],{"class":416,"line":494},[414,817,818],{"class":420},"\u002F**\n",[414,820,821],{"class":416,"line":559},[414,822,823],{"class":420}," * Returns true when the user prefers reduced motion.\n",[414,825,826],{"class":416,"line":584},[414,827,828],{"class":420}," * Defaults to `true` so SSR and first paint assume the safe, low-motion\n",[414,830,831],{"class":416,"line":590},[414,832,833],{"class":420}," * state — a vestibular user never sees a large animation before hydration.\n",[414,835,837],{"class":416,"line":836},11,[414,838,839],{"class":420}," *\u002F\n",[414,841,843,846,849,852,855,857,860],{"class":416,"line":842},12,[414,844,845],{"class":448},"export",[414,847,848],{"class":448}," function",[414,850,851],{"class":427}," useReducedMotion",[414,853,854],{"class":431},"()",[414,856,403],{"class":448},[414,858,859],{"class":438}," boolean",[414,861,432],{"class":431},[414,863,865,868,871,874,877,880,883,886,889,892,895],{"class":416,"line":864},13,[414,866,867],{"class":448},"  const",[414,869,870],{"class":431}," [",[414,872,873],{"class":438},"reduced",[414,875,876],{"class":431},", ",[414,878,879],{"class":438},"setReduced",[414,881,882],{"class":431},"] ",[414,884,885],{"class":448},"=",[414,887,888],{"class":427}," useState",[414,890,891],{"class":431},"(",[414,893,894],{"class":438},"true",[414,896,897],{"class":431},");\n",[414,899,901],{"class":416,"line":900},14,[414,902,543],{"emptyLinePlaceholder":542},[414,904,906,909,912,915],{"class":416,"line":905},15,[414,907,908],{"class":427},"  useEffect",[414,910,911],{"class":431},"(() ",[414,913,914],{"class":448},"=>",[414,916,432],{"class":431},[414,918,920,923,926,928,931,934,936,939,942],{"class":416,"line":919},16,[414,921,922],{"class":448},"    const",[414,924,925],{"class":438}," mql",[414,927,804],{"class":448},[414,929,930],{"class":431}," window.",[414,932,933],{"class":427},"matchMedia",[414,935,891],{"class":431},[414,937,938],{"class":438},"QUERY",[414,940,941],{"class":431},"); ",[414,943,944],{"class":420},"\u002F\u002F window only exists client-side\n",[414,946,948,951,954],{"class":416,"line":947},17,[414,949,950],{"class":427},"    setReduced",[414,952,953],{"class":431},"(mql.matches);              ",[414,955,956],{"class":420},"\u002F\u002F sync to the real value after mount\n",[414,958,960],{"class":416,"line":959},18,[414,961,543],{"emptyLinePlaceholder":542},[414,963,965,967,970,972,975,979,981,984,987,989,992],{"class":416,"line":964},19,[414,966,922],{"class":448},[414,968,969],{"class":427}," onChange",[414,971,804],{"class":448},[414,973,974],{"class":431}," (",[414,976,978],{"class":977},"s4XuR","event",[414,980,403],{"class":448},[414,982,983],{"class":427}," MediaQueryListEvent",[414,985,986],{"class":431},") ",[414,988,914],{"class":448},[414,990,991],{"class":427}," setReduced",[414,993,994],{"class":431},"(event.matches);\n",[414,996,998,1001,1004,1006,1009,1012],{"class":416,"line":997},20,[414,999,1000],{"class":431},"    mql.",[414,1002,1003],{"class":427},"addEventListener",[414,1005,891],{"class":431},[414,1007,1008],{"class":766},"'change'",[414,1010,1011],{"class":431},", onChange); ",[414,1013,1014],{"class":420},"\u002F\u002F respect a mid-session OS toggle\n",[414,1016,1018,1021,1024,1026,1029,1032,1034,1036],{"class":416,"line":1017},21,[414,1019,1020],{"class":448},"    return",[414,1022,1023],{"class":431}," () ",[414,1025,914],{"class":448},[414,1027,1028],{"class":431}," mql.",[414,1030,1031],{"class":427},"removeEventListener",[414,1033,891],{"class":431},[414,1035,1008],{"class":766},[414,1037,1038],{"class":431},", onChange);\n",[414,1040,1042],{"class":416,"line":1041},22,[414,1043,1044],{"class":431},"  }, []);\n",[414,1046,1048],{"class":416,"line":1047},23,[414,1049,543],{"emptyLinePlaceholder":542},[414,1051,1053,1056],{"class":416,"line":1052},24,[414,1054,1055],{"class":448},"  return",[414,1057,1058],{"class":431}," reduced;\n",[414,1060,1062],{"class":416,"line":1061},25,[414,1063,461],{"class":431},[324,1065,1066,1067,1070,1071,1074,1075,1080],{},"Two design choices carry the accessibility weight. The ",[501,1068,1069],{},"change listener"," means a user who flips \"Reduce motion\" in their OS while your app is open is honored immediately, with no reload—",[328,1072,1073],{},"matchMedia().matches"," read once would silently ignore them. The ",[501,1076,1077,1078],{},"default of ",[328,1079,894],{}," is the fail-safe direction discussed under the SSR caveat below.",[324,1082,1083,1084,1087],{},"This hook belongs alongside your other accessibility utilities; see ",[345,1085,181],{"href":1086},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F"," for the broader pattern of wrapping browser accessibility APIs in reusable hooks.",[350,1089],{},[353,1091,1093],{"id":1092},"gating-js-and-framer-motion-animations","Gating JS and Framer Motion Animations",[324,1095,1096],{},"With the hook in hand, branch your animation on its value. The pattern is one component with two presentation paths—reduced motion gets a crossfade, not a removed transition, so feedback survives.",[405,1098,1100],{"className":757,"code":1099,"language":759,"meta":410,"style":410},"'use client';\n\nimport { motion } from 'framer-motion';\nimport { useReducedMotion } from '.\u002FuseReducedMotion';\n\nexport function Drawer({ children }: { children: React.ReactNode }) {\n  const reduced = useReducedMotion();\n\n  \u002F\u002F Full variant slides in from the side; reduced variant only fades.\n  const variants = reduced\n    ? { closed: { opacity: 0 }, open: { opacity: 1 } }\n    : { closed: { opacity: 0, x: -320 }, open: { opacity: 1, x: 0 } };\n\n  return (\n    \u003Cmotion.aside\n      initial=\"closed\"\n      animate=\"open\"\n      exit=\"closed\"\n      variants={variants}\n      transition={{ duration: reduced ? 0.12 : 0.3 }}\n    >\n      {children}\n    \u003C\u002Fmotion.aside>\n  );\n}\n",[328,1101,1102,1108,1112,1126,1140,1144,1181,1195,1199,1204,1216,1235,1264,1268,1275,1283,1293,1303,1312,1322,1347,1352,1357,1368,1373],{"__ignoreMap":410},[414,1103,1104,1106],{"class":416,"line":417},[414,1105,767],{"class":766},[414,1107,455],{"class":431},[414,1109,1110],{"class":416,"line":424},[414,1111,543],{"emptyLinePlaceholder":542},[414,1113,1114,1116,1119,1121,1124],{"class":416,"line":435},[414,1115,778],{"class":448},[414,1117,1118],{"class":431}," { motion } ",[414,1120,784],{"class":448},[414,1122,1123],{"class":766}," 'framer-motion'",[414,1125,455],{"class":431},[414,1127,1128,1130,1133,1135,1138],{"class":416,"line":458},[414,1129,778],{"class":448},[414,1131,1132],{"class":431}," { useReducedMotion } ",[414,1134,784],{"class":448},[414,1136,1137],{"class":766}," '.\u002FuseReducedMotion'",[414,1139,455],{"class":431},[414,1141,1142],{"class":416,"line":464},[414,1143,543],{"emptyLinePlaceholder":542},[414,1145,1146,1148,1150,1153,1156,1159,1162,1164,1166,1168,1170,1173,1175,1178],{"class":416,"line":473},[414,1147,845],{"class":448},[414,1149,848],{"class":448},[414,1151,1152],{"class":427}," Drawer",[414,1154,1155],{"class":431},"({ ",[414,1157,1158],{"class":977},"children",[414,1160,1161],{"class":431}," }",[414,1163,403],{"class":448},[414,1165,479],{"class":431},[414,1167,1158],{"class":977},[414,1169,403],{"class":448},[414,1171,1172],{"class":427}," React",[414,1174,348],{"class":431},[414,1176,1177],{"class":427},"ReactNode",[414,1179,1180],{"class":431}," }) {\n",[414,1182,1183,1185,1188,1190,1192],{"class":416,"line":494},[414,1184,867],{"class":448},[414,1186,1187],{"class":438}," reduced",[414,1189,804],{"class":448},[414,1191,851],{"class":427},[414,1193,1194],{"class":431},"();\n",[414,1196,1197],{"class":416,"line":559},[414,1198,543],{"emptyLinePlaceholder":542},[414,1200,1201],{"class":416,"line":584},[414,1202,1203],{"class":420},"  \u002F\u002F Full variant slides in from the side; reduced variant only fades.\n",[414,1205,1206,1208,1211,1213],{"class":416,"line":590},[414,1207,867],{"class":448},[414,1209,1210],{"class":438}," variants",[414,1212,804],{"class":448},[414,1214,1215],{"class":431}," reduced\n",[414,1217,1218,1221,1224,1227,1230,1232],{"class":416,"line":836},[414,1219,1220],{"class":448},"    ?",[414,1222,1223],{"class":431}," { closed: { opacity: ",[414,1225,1226],{"class":438},"0",[414,1228,1229],{"class":431}," }, open: { opacity: ",[414,1231,681],{"class":438},[414,1233,1234],{"class":431}," } }\n",[414,1236,1237,1240,1242,1244,1247,1250,1253,1255,1257,1259,1261],{"class":416,"line":842},[414,1238,1239],{"class":448},"    :",[414,1241,1223],{"class":431},[414,1243,1226],{"class":438},[414,1245,1246],{"class":431},", x: ",[414,1248,1249],{"class":448},"-",[414,1251,1252],{"class":438},"320",[414,1254,1229],{"class":431},[414,1256,681],{"class":438},[414,1258,1246],{"class":431},[414,1260,1226],{"class":438},[414,1262,1263],{"class":431}," } };\n",[414,1265,1266],{"class":416,"line":864},[414,1267,543],{"emptyLinePlaceholder":542},[414,1269,1270,1272],{"class":416,"line":900},[414,1271,1055],{"class":448},[414,1273,1274],{"class":431}," (\n",[414,1276,1277,1280],{"class":416,"line":905},[414,1278,1279],{"class":431},"    \u003C",[414,1281,1282],{"class":438},"motion.aside\n",[414,1284,1285,1288,1290],{"class":416,"line":919},[414,1286,1287],{"class":427},"      initial",[414,1289,885],{"class":448},[414,1291,1292],{"class":766},"\"closed\"\n",[414,1294,1295,1298,1300],{"class":416,"line":947},[414,1296,1297],{"class":427},"      animate",[414,1299,885],{"class":448},[414,1301,1302],{"class":766},"\"open\"\n",[414,1304,1305,1308,1310],{"class":416,"line":959},[414,1306,1307],{"class":427},"      exit",[414,1309,885],{"class":448},[414,1311,1292],{"class":766},[414,1313,1314,1317,1319],{"class":416,"line":964},[414,1315,1316],{"class":427},"      variants",[414,1318,885],{"class":448},[414,1320,1321],{"class":431},"{variants}\n",[414,1323,1324,1327,1329,1332,1335,1338,1341,1344],{"class":416,"line":997},[414,1325,1326],{"class":427},"      transition",[414,1328,885],{"class":448},[414,1330,1331],{"class":431},"{{ duration: reduced ",[414,1333,1334],{"class":448},"?",[414,1336,1337],{"class":438}," 0.12",[414,1339,1340],{"class":448}," :",[414,1342,1343],{"class":438}," 0.3",[414,1345,1346],{"class":431}," }}\n",[414,1348,1349],{"class":416,"line":1017},[414,1350,1351],{"class":431},"    >\n",[414,1353,1354],{"class":416,"line":1041},[414,1355,1356],{"class":431},"      {children}\n",[414,1358,1359,1362,1365],{"class":416,"line":1047},[414,1360,1361],{"class":431},"    \u003C\u002F",[414,1363,1364],{"class":438},"motion.aside",[414,1366,1367],{"class":431},">\n",[414,1369,1370],{"class":416,"line":1052},[414,1371,1372],{"class":431},"  );\n",[414,1374,1375],{"class":416,"line":1061},[414,1376,461],{"class":431},[324,1378,1379,1381,1382,1384,1385,1388,1389,1392,1393,1395,1396,1399,1400,348],{},[328,1380,330],{}," also offers a library-native route that avoids per-component branching. Its own ",[328,1383,342],{}," reads the same media feature, and wrapping your tree in ",[328,1386,1387],{},"\u003CMotionConfig reducedMotion=\"user\">"," makes every nested ",[328,1390,1391],{},"motion"," element automatically drop transform and layout animations while keeping opacity. If you already depend on ",[328,1394,330],{},", prefer ",[328,1397,1398],{},"MotionConfig"," for app-wide coverage and reserve a custom hook for non-Framer code like GSAP or raw ",[328,1401,334],{},[324,1403,1404],{},"For an imperative animation loop, the gate is just an early branch:",[405,1406,1408],{"className":757,"code":1407,"language":759,"meta":410,"style":410},"const reduced = useReducedMotion();\n\nuseEffect(() => {\n  if (reduced) {\n    element.style.transform = 'none'; \u002F\u002F jump straight to the end state\n    return;\n  }\n  \u002F\u002F ...start the requestAnimationFrame tween only when motion is welcome\n}, [reduced]);\n",[328,1409,1410,1422,1426,1436,1444,1459,1465,1469,1474],{"__ignoreMap":410},[414,1411,1412,1414,1416,1418,1420],{"class":416,"line":417},[414,1413,798],{"class":448},[414,1415,1187],{"class":438},[414,1417,804],{"class":448},[414,1419,851],{"class":427},[414,1421,1194],{"class":431},[414,1423,1424],{"class":416,"line":424},[414,1425,543],{"emptyLinePlaceholder":542},[414,1427,1428,1430,1432,1434],{"class":416,"line":435},[414,1429,362],{"class":427},[414,1431,911],{"class":431},[414,1433,914],{"class":448},[414,1435,432],{"class":431},[414,1437,1438,1441],{"class":416,"line":458},[414,1439,1440],{"class":448},"  if",[414,1442,1443],{"class":431}," (reduced) {\n",[414,1445,1446,1449,1451,1454,1456],{"class":416,"line":464},[414,1447,1448],{"class":431},"    element.style.transform ",[414,1450,885],{"class":448},[414,1452,1453],{"class":766}," 'none'",[414,1455,686],{"class":431},[414,1457,1458],{"class":420},"\u002F\u002F jump straight to the end state\n",[414,1460,1461,1463],{"class":416,"line":473},[414,1462,1020],{"class":448},[414,1464,455],{"class":431},[414,1466,1467],{"class":416,"line":494},[414,1468,587],{"class":431},[414,1470,1471],{"class":416,"line":559},[414,1472,1473],{"class":420},"  \u002F\u002F ...start the requestAnimationFrame tween only when motion is welcome\n",[414,1475,1476],{"class":416,"line":584},[414,1477,1478],{"class":431},"}, [reduced]);\n",[350,1480],{},[353,1482,1484],{"id":1483},"the-ssr-caveat","The SSR Caveat",[324,1486,1487,1488,1491,1492,1494],{},"Server-side rendering has no ",[328,1489,1490],{},"window",", so ",[328,1493,370],{}," cannot run during the server pass or it throws. Three rules keep the hook SSR-safe.",[324,1496,1497,1498,1506,1507,1510,1511,1513,1514,1516],{},"First, ",[501,1499,1500,1501,1503,1504],{},"only touch ",[328,1502,1490],{}," inside ",[328,1505,362],{}," (or a ",[328,1508,1509],{},"typeof window !== 'undefined'"," guard), never in the render body or a ",[328,1512,366],{}," initializer that runs on the server. Effects do not execute during SSR, so the read is deferred to the client where ",[328,1515,1490],{}," exists.",[324,1518,1519,1520,1525,1526,1529],{},"Second, ",[501,1521,1522,1523],{},"default to ",[328,1524,894],{},". The server has to render ",[387,1527,1528],{},"some"," value, and it cannot know the user's preference. Defaulting to reduced motion means the worst-case first paint is a missing animation—harmless—rather than a large spatial transition firing before hydration corrects it, which is exactly what a vestibular user disabled motion to avoid. Fail safe toward less motion.",[324,1531,1532,1533,1536,1537,1539],{},"Third, ",[501,1534,1535],{},"expect a brief mismatch and design for it",". The server renders the reduced state, then the effect runs on the client and may switch to full motion for a ",[328,1538,511],{}," user. Because the static state is your baseline, this resolves as \"animation becomes available\" rather than \"animation is yanked away\"—a non-disruptive direction. Avoid reading the preference during render to influence server markup; keep it in the effect so hydration stays stable.",[350,1541],{},[353,1543,1545],{"id":1544},"how-to-verify","How to Verify",[324,1547,1548],{},"Verification needs three layers: real OS behavior, fast emulation, and an automated regression guard.",[324,1550,1551,1554],{},[501,1552,1553],{},"1. OS setting (the source of truth)."," Enable the system toggle and reload:",[1556,1557,1558,1565,1572,1578],"ul",{},[1559,1560,1561,1562],"li",{},"macOS: System Settings → Accessibility → Display → ",[501,1563,1564],{},"Reduce motion",[1559,1566,1567,1568,1571],{},"Windows: Settings → Accessibility → Visual effects → ",[501,1569,1570],{},"Animation effects"," off",[1559,1573,1574,1575],{},"iOS: Settings → Accessibility → Motion → ",[501,1576,1577],{},"Reduce Motion",[1559,1579,1580,1581],{},"GNOME\u002FLinux: Settings → Accessibility → ",[501,1582,1583],{},"Reduce Animation",[324,1585,1586,1587,1589,1590,1593],{},"Confirm CSS transitions collapse and ",[328,1588,330],{}," components fall back to their crossfade variant. Then toggle the setting ",[387,1591,1592],{},"with the app still open","—the change listener should switch behavior without a reload.",[324,1595,1596,1599,1600,1603,1604,1606,1607,1610,1611,1614,1615,1617],{},[501,1597,1598],{},"2. DevTools emulation (fast iteration)."," In Chrome, open DevTools → Rendering panel → ",[501,1601,1602],{},"Emulate CSS media feature prefers-reduced-motion"," and set it to ",[328,1605,402],{},". Firefox exposes ",[328,1608,1609],{},"ui.prefersReducedMotion"," in ",[328,1612,1613],{},"about:config",". Emulation drives the media query but does not always fire ",[328,1616,933],{}," change events, so use it for CSS checks and the OS toggle for the listener.",[324,1619,1620,1623,1624,1626],{},[501,1621,1622],{},"3. An automated test."," Mock ",[328,1625,933],{}," in jsdom (it is undefined there by default) and assert the hook returns the mocked value:",[405,1628,1630],{"className":757,"code":1629,"language":759,"meta":410,"style":410},"import { renderHook } from '@testing-library\u002Freact';\nimport { useReducedMotion } from '.\u002FuseReducedMotion';\n\nfunction mockMatchMedia(matches: boolean) {\n  window.matchMedia = (query: string) => ({\n    matches,\n    media: query,\n    addEventListener: () => {},\n    removeEventListener: () => {},\n    addListener: () => {},\n    removeListener: () => {},\n    onchange: null,\n    dispatchEvent: () => false,\n  } as MediaQueryList);\n}\n\ntest('reports reduced motion when the OS prefers it', () => {\n  mockMatchMedia(true);\n  const { result } = renderHook(() => useReducedMotion());\n  expect(result.current).toBe(true);\n});\n",[328,1631,1632,1646,1658,1662,1682,1708,1713,1718,1731,1742,1753,1764,1774,1788,1801,1805,1809,1826,1837,1863,1880],{"__ignoreMap":410},[414,1633,1634,1636,1639,1641,1644],{"class":416,"line":417},[414,1635,778],{"class":448},[414,1637,1638],{"class":431}," { renderHook } ",[414,1640,784],{"class":448},[414,1642,1643],{"class":766}," '@testing-library\u002Freact'",[414,1645,455],{"class":431},[414,1647,1648,1650,1652,1654,1656],{"class":416,"line":424},[414,1649,778],{"class":448},[414,1651,1132],{"class":431},[414,1653,784],{"class":448},[414,1655,1137],{"class":766},[414,1657,455],{"class":431},[414,1659,1660],{"class":416,"line":435},[414,1661,543],{"emptyLinePlaceholder":542},[414,1663,1664,1667,1670,1672,1675,1677,1679],{"class":416,"line":458},[414,1665,1666],{"class":448},"function",[414,1668,1669],{"class":427}," mockMatchMedia",[414,1671,891],{"class":431},[414,1673,1674],{"class":977},"matches",[414,1676,403],{"class":448},[414,1678,859],{"class":438},[414,1680,1681],{"class":431},") {\n",[414,1683,1684,1687,1689,1691,1693,1696,1698,1701,1703,1705],{"class":416,"line":464},[414,1685,1686],{"class":431},"  window.",[414,1688,933],{"class":427},[414,1690,804],{"class":448},[414,1692,974],{"class":431},[414,1694,1695],{"class":977},"query",[414,1697,403],{"class":448},[414,1699,1700],{"class":438}," string",[414,1702,986],{"class":431},[414,1704,914],{"class":448},[414,1706,1707],{"class":431}," ({\n",[414,1709,1710],{"class":416,"line":473},[414,1711,1712],{"class":431},"    matches,\n",[414,1714,1715],{"class":416,"line":494},[414,1716,1717],{"class":431},"    media: query,\n",[414,1719,1720,1723,1726,1728],{"class":416,"line":559},[414,1721,1722],{"class":427},"    addEventListener",[414,1724,1725],{"class":431},": () ",[414,1727,914],{"class":448},[414,1729,1730],{"class":431}," {},\n",[414,1732,1733,1736,1738,1740],{"class":416,"line":584},[414,1734,1735],{"class":427},"    removeEventListener",[414,1737,1725],{"class":431},[414,1739,914],{"class":448},[414,1741,1730],{"class":431},[414,1743,1744,1747,1749,1751],{"class":416,"line":590},[414,1745,1746],{"class":427},"    addListener",[414,1748,1725],{"class":431},[414,1750,914],{"class":448},[414,1752,1730],{"class":431},[414,1754,1755,1758,1760,1762],{"class":416,"line":836},[414,1756,1757],{"class":427},"    removeListener",[414,1759,1725],{"class":431},[414,1761,914],{"class":448},[414,1763,1730],{"class":431},[414,1765,1766,1769,1772],{"class":416,"line":842},[414,1767,1768],{"class":431},"    onchange: ",[414,1770,1771],{"class":438},"null",[414,1773,636],{"class":431},[414,1775,1776,1779,1781,1783,1786],{"class":416,"line":864},[414,1777,1778],{"class":427},"    dispatchEvent",[414,1780,1725],{"class":431},[414,1782,914],{"class":448},[414,1784,1785],{"class":438}," false",[414,1787,636],{"class":431},[414,1789,1790,1793,1796,1799],{"class":416,"line":900},[414,1791,1792],{"class":431},"  } ",[414,1794,1795],{"class":448},"as",[414,1797,1798],{"class":427}," MediaQueryList",[414,1800,897],{"class":431},[414,1802,1803],{"class":416,"line":905},[414,1804,461],{"class":431},[414,1806,1807],{"class":416,"line":919},[414,1808,543],{"emptyLinePlaceholder":542},[414,1810,1811,1814,1816,1819,1822,1824],{"class":416,"line":947},[414,1812,1813],{"class":427},"test",[414,1815,891],{"class":431},[414,1817,1818],{"class":766},"'reports reduced motion when the OS prefers it'",[414,1820,1821],{"class":431},", () ",[414,1823,914],{"class":448},[414,1825,432],{"class":431},[414,1827,1828,1831,1833,1835],{"class":416,"line":959},[414,1829,1830],{"class":427},"  mockMatchMedia",[414,1832,891],{"class":431},[414,1834,894],{"class":438},[414,1836,897],{"class":431},[414,1838,1839,1841,1843,1846,1849,1851,1854,1856,1858,1860],{"class":416,"line":964},[414,1840,867],{"class":448},[414,1842,479],{"class":431},[414,1844,1845],{"class":438},"result",[414,1847,1848],{"class":431}," } ",[414,1850,885],{"class":448},[414,1852,1853],{"class":427}," renderHook",[414,1855,911],{"class":431},[414,1857,914],{"class":448},[414,1859,851],{"class":427},[414,1861,1862],{"class":431},"());\n",[414,1864,1865,1868,1871,1874,1876,1878],{"class":416,"line":997},[414,1866,1867],{"class":427},"  expect",[414,1869,1870],{"class":431},"(result.current).",[414,1872,1873],{"class":427},"toBe",[414,1875,891],{"class":431},[414,1877,894],{"class":438},[414,1879,897],{"class":431},[414,1881,1882],{"class":416,"line":1017},[414,1883,1884],{"class":431},"});\n",[350,1886],{},[353,1888,1890],{"id":1889},"common-a11y-mistakes","Common a11y Mistakes",[1556,1892,1893,1906,1917,1928,1945,1956],{},[1559,1894,1895,1901,1902,1905],{},[501,1896,1897,1898,1900],{},"Reading ",[328,1899,933],{}," once, no listener:"," Ignores a mid-session OS toggle. Always subscribe to the ",[328,1903,1904],{},"change"," event.",[1559,1907,1908,1914,1915,348],{},[501,1909,1910,1911,403],{},"Defaulting the hook to ",[328,1912,1913],{},"false"," Ships motion on first paint before hydration. Default to ",[328,1916,894],{},[1559,1918,1919,1925,1926,348],{},[501,1920,1921,1922,1924],{},"Calling ",[328,1923,370],{}," in the render body:"," Throws during SSR. Confine it to ",[328,1927,362],{},[1559,1929,1930,1937,1938,1940,1941,1944],{},[501,1931,1932,1933,1936],{},"Using ",[328,1934,1935],{},"transition: none"," in the global guard:"," Breaks JavaScript that waits on ",[328,1939,611],{},". Use a ",[328,1942,1943],{},"0.01ms"," duration instead.",[1559,1946,1947,1950,1951,1953,1954,348],{},[501,1948,1949],{},"Gating only CSS:"," A ",[328,1952,330],{}," spring or GSAP timeline keeps running because CSS media queries do not reach it. Gate it on the hook or ",[328,1955,1398],{},[1559,1957,1958,1964,1965,1967],{},[501,1959,1960,1961,1963],{},"Reactive ",[328,1962,402],{}," overrides only:"," Forgetting one override ships unwanted motion. Prefer the motion-safe ",[328,1966,511],{}," default.",[350,1969],{},[353,1971,1973],{"id":1972},"frequently-asked-questions","Frequently Asked Questions",[324,1975,1976,1979,1980,876,1982,1985,1986,1988],{},[501,1977,1978],{},"Do I need both the CSS media query and the React hook?","\nYes. The media query governs declarative CSS transitions and keyframes with no hydration risk, while the hook is the only way to gate JavaScript-driven animation like ",[328,1981,330],{},[328,1983,1984],{},"react-spring",", GSAP, or ",[328,1987,334],{}," loops. They cover different runtimes.",[324,1990,1991,1999,2000,2002],{},[501,1992,1993,1994,1996,1997,1334],{},"Why does the hook default to ",[328,1995,894],{}," (reduced) instead of ",[328,1998,1913],{},"\nThe server has no ",[328,2001,1490],{}," and cannot know the preference, so it must render a default. Defaulting to reduced means first paint is static—at worst a missing flourish for a no-preference user, which resolves harmlessly after hydration—rather than an unwanted large animation firing for someone who disabled motion.",[324,2004,2005,2008,2009,2011,2012,363,2014,2016,2017,2019],{},[501,2006,2007],{},"Should I use Framer Motion's built-in support or my own hook?","\nIf you already depend on ",[328,2010,330],{},", use its ",[328,2013,342],{},[328,2015,1387],{}," for automatic app-wide coverage. Use a custom hook for non-Framer animation—GSAP, raw ",[328,2018,334],{},", or conditional logic that the library cannot see.",[324,2021,2022,2025,2026,2028],{},[501,2023,2024],{},"Will DevTools emulation fully test my hook?","\nIt reliably drives the CSS media query, but emulation does not always dispatch ",[328,2027,933],{}," change events, so it cannot exercise the live-toggle path. Verify the change listener with the real OS setting toggled while the app is open.",[350,2030],{},[353,2032,2034],{"id":2033},"related-guides","Related guides",[1556,2036,2037,2041,2046],{},[1559,2038,2039],{},[345,2040,49],{"href":347},[1559,2042,2043],{},[345,2044,55],{"href":2045},"\u002Fcore-accessibility-principles-for-modern-frameworks\u002Freduced-motion-and-animation-accessibility\u002Faccessible-loading-skeletons-and-spinners\u002F",[1559,2047,2048],{},[345,2049,181],{"href":1086},[2051,2052,2053],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}",{"title":410,"searchDepth":424,"depth":424,"links":2055},[2056,2057,2058,2060,2061,2062,2063,2064,2065],{"id":355,"depth":424,"text":356},{"id":395,"depth":424,"text":396},{"id":738,"depth":424,"text":2059},"A useReducedMotion() Hook",{"id":1092,"depth":424,"text":1093},{"id":1483,"depth":424,"text":1484},{"id":1544,"depth":424,"text":1545},{"id":1889,"depth":424,"text":1890},{"id":1972,"depth":424,"text":1973},{"id":2033,"depth":424,"text":2034},null,"Honor the user's motion preference everywhere—a CSS-first pattern plus a useReducedMotion React hook so transitions, JS animations, and Framer Motion all adapt.","md",{},false,{"title":61,"description":2067},"hWZ3xL83lroKurI7GsCzbNKu_kLq6JyE4N2PhuS3Q-M",[2074,2113,2114,2177],{"title":5,"path":6,"stem":7,"children":2075},[2076,2077,2080,2083,2089,2095,2104,2110],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2078},[2079],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2081},[2082],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2084},[2085,2086],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2087},[2088],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2090},[2091,2092],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2093},[2094],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2096},[2097,2098,2101],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2099},[2100],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2102},[2103],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2105},[2106,2107],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2108},[2109],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2111},[2112],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2115},[2116,2117,2123,2135,2147,2150,2159,2171],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2118},[2119,2120],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2121},[2122],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2124},[2125,2126,2129,2132],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2127},[2128],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2130},[2131],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2133},[2134],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2136},[2137,2138,2141,2144],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2139},[2140],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2142},[2143],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2145},[2146],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2148},[2149],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2151},[2152,2153,2156],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2154},[2155],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2157},[2158],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2160},[2161,2162,2165,2168],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2163},[2164],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2166},[2167],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2169},[2170],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2172},[2173,2174],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2175},[2176],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2178},[2179,2180,2189,2198,2207,2216],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2181},[2182,2183,2186],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2184},[2185],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2187},[2188],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2190},[2191,2192,2195],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2193},[2194],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2196},[2197],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2199},[2200,2201,2204],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2202},[2203],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2205},[2206],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2208},[2209,2210,2213],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2211},[2212],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2214},[2215],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2217},[2218,2219,2222],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2220},[2221],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2223},[2224],{"title":309,"path":310,"stem":311},1781785523922]