[{"data":1,"prerenderedAt":1832},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002F":314,"content-navigation":1680},[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":127,"body":316,"date":1673,"description":1674,"extension":1675,"image":1673,"meta":1676,"modifiedAt":1673,"navigation":540,"noindex":1677,"path":128,"publishedAt":1673,"seo":1678,"stem":129,"updatedAt":1673,"__hash__":1679},"content\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002Findex.md",{"type":317,"value":318,"toc":1662},"minimark",[319,323,332,344,350,375,378,383,386,413,429,431,439,452,489,1190,1206,1220,1222,1226,1237,1282,1405,1411,1413,1417,1423,1434,1440,1442,1446,1500,1515,1517,1521,1576,1578,1582,1592,1608,1630,1639,1641,1645,1659],[320,321,127],"h1",{"id":322},"virtualizing-long-lists-accessibly-in-react",[324,325,326,327,331],"p",{},"Virtualization makes 50,000-row tables fast by rendering only the handful of rows currently on screen. It also quietly destroys accessibility: the DOM now holds a dozen rows instead of fifty thousand, so a screen reader reports a tiny table, focused rows vanish as the user scrolls, and the structure assistive technology relies on falls apart. This guide shows how to virtualize with react-window or ",[328,329,330],"code",{},"@tanstack\u002Freact-virtual"," while preserving the true size of the data and keeping focus stable as rows recycle.",[324,333,334,335,339,340,343],{},"This is a deep dive under ",[336,337,109],"a",{"href":338},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002F",". Read that guide first for the semantic-table foundations and the discussion of when ",[328,341,342],{},"role=\"grid\""," is appropriate—virtualization is one of the few cases where it genuinely is.",[324,345,346],{},[347,348,349],"strong",{},"Mapped WCAG Success Criteria:",[351,352,353,360,365,370],"ul",{},[354,355,356,359],"li",{},[328,357,358],{},"1.3.1 Info and Relationships"," (Level A)",[354,361,362,359],{},[328,363,364],{},"4.1.2 Name, Role, Value",[354,366,367,359],{},[328,368,369],{},"2.1.1 Keyboard",[354,371,372,359],{},[328,373,374],{},"2.4.3 Focus Order",[376,377],"hr",{},[379,380,382],"h2",{"id":381},"why-virtualization-breaks-the-accessibility-tree","Why Virtualization Breaks the Accessibility Tree",[324,384,385],{},"The accessibility tree is built from the DOM. Virtualization's entire purpose is to keep most of the DOM from existing—only the rows in (and just around) the viewport are mounted; the rest are absent. Three things break as a result:",[351,387,388,397,403],{},[354,389,390,393,394,396],{},[347,391,392],{},"Size is wrong."," A screen reader counts the rows it can see in the DOM. With windowing, that might be 12 rows of a 50,000-row dataset, so the user is told the table has 12 rows. The relationship between a row and the whole dataset, required by ",[328,395,358],{},", is lost.",[354,398,399,402],{},[347,400,401],{},"Position is wrong."," Even if a row is rendered, nothing tells the user it is row 8,400 of 50,000. \"Row 5 of 12\" is actively misleading.",[354,404,405,408,409,412],{},[347,406,407],{},"Focus disappears."," When the user scrolls or navigates, the row that had focus unmounts. The browser drops focus to ",[328,410,411],{},"\u003Cbody>",", stranding keyboard users.",[324,414,415,416,420,421,424,425,428],{},"The fix is to ",[417,418,419],"em",{},"tell"," assistive technology the truth about the data even though the DOM only holds a slice—via ",[328,422,423],{},"aria-rowcount"," and ",[328,426,427],{},"aria-rowindex","—and to manage focus deliberately as rows recycle.",[376,430],{},[379,432,434,435,424,437],{"id":433},"restoring-true-size-with-aria-rowcount-and-aria-rowindex","Restoring True Size With ",[328,436,423],{},[328,438,427],{},[324,440,441,442,444,445,448,449,451],{},"ARIA provides attributes precisely for \"the DOM holds fewer rows than really exist.\" They are defined for grid and table roles, so a virtualized data table that needs them adopts ",[328,443,342],{}," (or keeps a native ",[328,446,447],{},"\u003Ctable>"," augmented with these attributes). This satisfies ",[328,450,364],{}," by reporting the real dimensions:",[351,453,454,467,480],{},[354,455,456,458,459,462,463,466],{},[328,457,423],{}," on the grid declares the ",[347,460,461],{},"total"," number of rows in the full dataset, not the number currently in the DOM. Use ",[328,464,465],{},"-1"," only if the total is genuinely unknown (e.g., infinite streaming).",[354,468,469,471,472,475,476,479],{},[328,470,427],{}," on each rendered row declares its ",[347,473,474],{},"1-based position within the full dataset",", so row 8,400 announces as row 8,400 even though it is the third ",[328,477,478],{},"\u003Cdiv>"," in the DOM.",[354,481,482,424,485,488],{},[328,483,484],{},"aria-colcount",[328,486,487],{},"aria-colindex"," do the same for columns when columns are also virtualized.",[490,491,496],"pre",{"className":492,"code":493,"language":494,"meta":495,"style":495},"language-tsx shiki shiki-themes github-light github-dark","'use client';\nimport { FixedSizeList, type ListChildComponentProps } from 'react-window';\n\nconst COLUMNS = ['Region', 'Revenue', 'Growth'];\n\nexport function VirtualGrid({ rows }: { rows: Row[] }) {\n  return (\n    \u002F\u002F aria-rowcount = the TRUE total, not the windowed count.\n    \u002F\u002F +1 accounts for the header row, which is also counted.\n    \u003Cdiv\n      role=\"grid\"\n      aria-label=\"Revenue by region\"\n      aria-rowcount={rows.length + 1}\n      aria-colcount={COLUMNS.length}\n    >\n      {\u002F* Header row is aria-rowindex 1 *\u002F}\n      \u003Cdiv role=\"row\" aria-rowindex={1} className=\"vg-header\">\n        {COLUMNS.map((c, i) => (\n          \u003Cspan role=\"columnheader\" aria-colindex={i + 1} key={c}>{c}\u003C\u002Fspan>\n        ))}\n      \u003C\u002Fdiv>\n\n      \u003CFixedSizeList\n        height={480}\n        itemCount={rows.length}\n        itemSize={40}\n        width=\"100%\"\n      >\n        {({ index, style }: ListChildComponentProps) => {\n          const row = rows[index];\n          return (\n            \u003Cdiv\n              role=\"row\"\n              style={style}\n              \u002F\u002F +2: header is row 1, and aria-rowindex is 1-based\n              aria-rowindex={index + 2}\n            >\n              \u003Cspan role=\"gridcell\" aria-colindex={1}>{row.region}\u003C\u002Fspan>\n              \u003Cspan role=\"gridcell\" aria-colindex={2}>{row.revenue}\u003C\u002Fspan>\n              \u003Cspan role=\"gridcell\" aria-colindex={3}>{row.growth}\u003C\u002Fspan>\n            \u003C\u002Fdiv>\n          );\n        }}\n      \u003C\u002FFixedSizeList>\n    \u003C\u002Fdiv>\n  );\n}\n","tsx","",[328,497,498,511,535,542,575,580,619,628,635,641,651,663,674,697,718,724,735,776,808,850,856,866,871,879,894,908,923,934,940,967,981,989,997,1008,1019,1025,1043,1049,1079,1108,1137,1147,1153,1159,1169,1179,1185],{"__ignoreMap":495},[499,500,503,507],"span",{"class":501,"line":502},"line",1,[499,504,506],{"class":505},"sZZnC","'use client'",[499,508,510],{"class":509},"sVt8B",";\n",[499,512,514,518,521,524,527,530,533],{"class":501,"line":513},2,[499,515,517],{"class":516},"szBVR","import",[499,519,520],{"class":509}," { FixedSizeList, ",[499,522,523],{"class":516},"type",[499,525,526],{"class":509}," ListChildComponentProps } ",[499,528,529],{"class":516},"from",[499,531,532],{"class":505}," 'react-window'",[499,534,510],{"class":509},[499,536,538],{"class":501,"line":537},3,[499,539,541],{"emptyLinePlaceholder":540},true,"\n",[499,543,545,548,552,555,558,561,564,567,569,572],{"class":501,"line":544},4,[499,546,547],{"class":516},"const",[499,549,551],{"class":550},"sj4cs"," COLUMNS",[499,553,554],{"class":516}," =",[499,556,557],{"class":509}," [",[499,559,560],{"class":505},"'Region'",[499,562,563],{"class":509},", ",[499,565,566],{"class":505},"'Revenue'",[499,568,563],{"class":509},[499,570,571],{"class":505},"'Growth'",[499,573,574],{"class":509},"];\n",[499,576,578],{"class":501,"line":577},5,[499,579,541],{"emptyLinePlaceholder":540},[499,581,583,586,589,593,596,600,603,606,609,611,613,616],{"class":501,"line":582},6,[499,584,585],{"class":516},"export",[499,587,588],{"class":516}," function",[499,590,592],{"class":591},"sScJk"," VirtualGrid",[499,594,595],{"class":509},"({ ",[499,597,599],{"class":598},"s4XuR","rows",[499,601,602],{"class":509}," }",[499,604,605],{"class":516},":",[499,607,608],{"class":509}," { ",[499,610,599],{"class":598},[499,612,605],{"class":516},[499,614,615],{"class":591}," Row",[499,617,618],{"class":509},"[] }) {\n",[499,620,622,625],{"class":501,"line":621},7,[499,623,624],{"class":516},"  return",[499,626,627],{"class":509}," (\n",[499,629,631],{"class":501,"line":630},8,[499,632,634],{"class":633},"sJ8bj","    \u002F\u002F aria-rowcount = the TRUE total, not the windowed count.\n",[499,636,638],{"class":501,"line":637},9,[499,639,640],{"class":633},"    \u002F\u002F +1 accounts for the header row, which is also counted.\n",[499,642,644,647],{"class":501,"line":643},10,[499,645,646],{"class":509},"    \u003C",[499,648,650],{"class":649},"s9eBZ","div\n",[499,652,654,657,660],{"class":501,"line":653},11,[499,655,656],{"class":591},"      role",[499,658,659],{"class":516},"=",[499,661,662],{"class":505},"\"grid\"\n",[499,664,666,669,671],{"class":501,"line":665},12,[499,667,668],{"class":591},"      aria-label",[499,670,659],{"class":516},[499,672,673],{"class":505},"\"Revenue by region\"\n",[499,675,677,680,682,685,688,691,694],{"class":501,"line":676},13,[499,678,679],{"class":591},"      aria-rowcount",[499,681,659],{"class":516},[499,683,684],{"class":509},"{rows.",[499,686,687],{"class":550},"length",[499,689,690],{"class":516}," +",[499,692,693],{"class":550}," 1",[499,695,696],{"class":509},"}\n",[499,698,700,703,705,708,711,714,716],{"class":501,"line":699},14,[499,701,702],{"class":591},"      aria-colcount",[499,704,659],{"class":516},[499,706,707],{"class":509},"{",[499,709,710],{"class":550},"COLUMNS",[499,712,713],{"class":509},".",[499,715,687],{"class":550},[499,717,696],{"class":509},[499,719,721],{"class":501,"line":720},15,[499,722,723],{"class":509},"    >\n",[499,725,727,730,733],{"class":501,"line":726},16,[499,728,729],{"class":509},"      {",[499,731,732],{"class":633},"\u002F* Header row is aria-rowindex 1 *\u002F",[499,734,696],{"class":509},[499,736,738,741,744,747,749,752,755,757,759,762,765,768,770,773],{"class":501,"line":737},17,[499,739,740],{"class":509},"      \u003C",[499,742,743],{"class":649},"div",[499,745,746],{"class":591}," role",[499,748,659],{"class":516},[499,750,751],{"class":505},"\"row\"",[499,753,754],{"class":591}," aria-rowindex",[499,756,659],{"class":516},[499,758,707],{"class":509},[499,760,761],{"class":550},"1",[499,763,764],{"class":509},"} ",[499,766,767],{"class":591},"className",[499,769,659],{"class":516},[499,771,772],{"class":505},"\"vg-header\"",[499,774,775],{"class":509},">\n",[499,777,779,782,784,786,789,792,795,797,800,803,806],{"class":501,"line":778},18,[499,780,781],{"class":509},"        {",[499,783,710],{"class":550},[499,785,713],{"class":509},[499,787,788],{"class":591},"map",[499,790,791],{"class":509},"((",[499,793,794],{"class":598},"c",[499,796,563],{"class":509},[499,798,799],{"class":598},"i",[499,801,802],{"class":509},") ",[499,804,805],{"class":516},"=>",[499,807,627],{"class":509},[499,809,811,814,816,818,820,823,826,828,831,834,836,838,841,843,846,848],{"class":501,"line":810},19,[499,812,813],{"class":509},"          \u003C",[499,815,499],{"class":649},[499,817,746],{"class":591},[499,819,659],{"class":516},[499,821,822],{"class":505},"\"columnheader\"",[499,824,825],{"class":591}," aria-colindex",[499,827,659],{"class":516},[499,829,830],{"class":509},"{i ",[499,832,833],{"class":516},"+",[499,835,693],{"class":550},[499,837,764],{"class":509},[499,839,840],{"class":591},"key",[499,842,659],{"class":516},[499,844,845],{"class":509},"{c}>{c}\u003C\u002F",[499,847,499],{"class":649},[499,849,775],{"class":509},[499,851,853],{"class":501,"line":852},20,[499,854,855],{"class":509},"        ))}\n",[499,857,859,862,864],{"class":501,"line":858},21,[499,860,861],{"class":509},"      \u003C\u002F",[499,863,743],{"class":649},[499,865,775],{"class":509},[499,867,869],{"class":501,"line":868},22,[499,870,541],{"emptyLinePlaceholder":540},[499,872,874,876],{"class":501,"line":873},23,[499,875,740],{"class":509},[499,877,878],{"class":550},"FixedSizeList\n",[499,880,882,885,887,889,892],{"class":501,"line":881},24,[499,883,884],{"class":591},"        height",[499,886,659],{"class":516},[499,888,707],{"class":509},[499,890,891],{"class":550},"480",[499,893,696],{"class":509},[499,895,897,900,902,904,906],{"class":501,"line":896},25,[499,898,899],{"class":591},"        itemCount",[499,901,659],{"class":516},[499,903,684],{"class":509},[499,905,687],{"class":550},[499,907,696],{"class":509},[499,909,911,914,916,918,921],{"class":501,"line":910},26,[499,912,913],{"class":591},"        itemSize",[499,915,659],{"class":516},[499,917,707],{"class":509},[499,919,920],{"class":550},"40",[499,922,696],{"class":509},[499,924,926,929,931],{"class":501,"line":925},27,[499,927,928],{"class":591},"        width",[499,930,659],{"class":516},[499,932,933],{"class":505},"\"100%\"\n",[499,935,937],{"class":501,"line":936},28,[499,938,939],{"class":509},"      >\n",[499,941,943,946,948,950,953,955,957,960,962,964],{"class":501,"line":942},29,[499,944,945],{"class":509},"        {({ ",[499,947,87],{"class":598},[499,949,563],{"class":509},[499,951,952],{"class":598},"style",[499,954,602],{"class":509},[499,956,605],{"class":516},[499,958,959],{"class":591}," ListChildComponentProps",[499,961,802],{"class":509},[499,963,805],{"class":516},[499,965,966],{"class":509}," {\n",[499,968,970,973,976,978],{"class":501,"line":969},30,[499,971,972],{"class":516},"          const",[499,974,975],{"class":550}," row",[499,977,554],{"class":516},[499,979,980],{"class":509}," rows[index];\n",[499,982,984,987],{"class":501,"line":983},31,[499,985,986],{"class":516},"          return",[499,988,627],{"class":509},[499,990,992,995],{"class":501,"line":991},32,[499,993,994],{"class":509},"            \u003C",[499,996,650],{"class":649},[499,998,1000,1003,1005],{"class":501,"line":999},33,[499,1001,1002],{"class":591},"              role",[499,1004,659],{"class":516},[499,1006,1007],{"class":505},"\"row\"\n",[499,1009,1011,1014,1016],{"class":501,"line":1010},34,[499,1012,1013],{"class":591},"              style",[499,1015,659],{"class":516},[499,1017,1018],{"class":509},"{style}\n",[499,1020,1022],{"class":501,"line":1021},35,[499,1023,1024],{"class":633},"              \u002F\u002F +2: header is row 1, and aria-rowindex is 1-based\n",[499,1026,1028,1031,1033,1036,1038,1041],{"class":501,"line":1027},36,[499,1029,1030],{"class":591},"              aria-rowindex",[499,1032,659],{"class":516},[499,1034,1035],{"class":509},"{index ",[499,1037,833],{"class":516},[499,1039,1040],{"class":550}," 2",[499,1042,696],{"class":509},[499,1044,1046],{"class":501,"line":1045},37,[499,1047,1048],{"class":509},"            >\n",[499,1050,1052,1055,1057,1059,1061,1064,1066,1068,1070,1072,1075,1077],{"class":501,"line":1051},38,[499,1053,1054],{"class":509},"              \u003C",[499,1056,499],{"class":649},[499,1058,746],{"class":591},[499,1060,659],{"class":516},[499,1062,1063],{"class":505},"\"gridcell\"",[499,1065,825],{"class":591},[499,1067,659],{"class":516},[499,1069,707],{"class":509},[499,1071,761],{"class":550},[499,1073,1074],{"class":509},"}>{row.region}\u003C\u002F",[499,1076,499],{"class":649},[499,1078,775],{"class":509},[499,1080,1082,1084,1086,1088,1090,1092,1094,1096,1098,1101,1104,1106],{"class":501,"line":1081},39,[499,1083,1054],{"class":509},[499,1085,499],{"class":649},[499,1087,746],{"class":591},[499,1089,659],{"class":516},[499,1091,1063],{"class":505},[499,1093,825],{"class":591},[499,1095,659],{"class":516},[499,1097,707],{"class":509},[499,1099,1100],{"class":550},"2",[499,1102,1103],{"class":509},"}>{row.revenue}\u003C\u002F",[499,1105,499],{"class":649},[499,1107,775],{"class":509},[499,1109,1111,1113,1115,1117,1119,1121,1123,1125,1127,1130,1133,1135],{"class":501,"line":1110},40,[499,1112,1054],{"class":509},[499,1114,499],{"class":649},[499,1116,746],{"class":591},[499,1118,659],{"class":516},[499,1120,1063],{"class":505},[499,1122,825],{"class":591},[499,1124,659],{"class":516},[499,1126,707],{"class":509},[499,1128,1129],{"class":550},"3",[499,1131,1132],{"class":509},"}>{row.growth}\u003C\u002F",[499,1134,499],{"class":649},[499,1136,775],{"class":509},[499,1138,1140,1143,1145],{"class":501,"line":1139},41,[499,1141,1142],{"class":509},"            \u003C\u002F",[499,1144,743],{"class":649},[499,1146,775],{"class":509},[499,1148,1150],{"class":501,"line":1149},42,[499,1151,1152],{"class":509},"          );\n",[499,1154,1156],{"class":501,"line":1155},43,[499,1157,1158],{"class":509},"        }}\n",[499,1160,1162,1164,1167],{"class":501,"line":1161},44,[499,1163,861],{"class":509},[499,1165,1166],{"class":550},"FixedSizeList",[499,1168,775],{"class":509},[499,1170,1172,1175,1177],{"class":501,"line":1171},45,[499,1173,1174],{"class":509},"    \u003C\u002F",[499,1176,743],{"class":649},[499,1178,775],{"class":509},[499,1180,1182],{"class":501,"line":1181},46,[499,1183,1184],{"class":509},"  );\n",[499,1186,1188],{"class":501,"line":1187},47,[499,1189,696],{"class":509},[324,1191,1192,1193,1196,1197,1199,1200,1202,1203,713],{},"With this in place, a screen reader announces \"row 8,400 of 50,001\" for a row that is physically the third element in the DOM. The ",[328,1194,1195],{},"+2"," offset on ",[328,1198,427],{}," is the detail people get wrong: the header occupies index 1, and ",[328,1201,427],{}," is 1-based, so the first data row (array index 0) is ",[328,1204,1205],{},"aria-rowindex={2}",[324,1207,1208,1209,1211,1212,1215,1216,1219],{},"Note that when you adopt ",[328,1210,342],{},", you take on the grid keyboard model: a single tab stop into the grid and arrow-key navigation between cells via roving ",[328,1213,1214],{},"tabindex"," or ",[328,1217,1218],{},"aria-activedescendant",". Do not adopt the grid role and then leave cells individually tabbable—that mixes two interaction models and confuses everyone.",[376,1221],{},[379,1223,1225],{"id":1224},"focus-management-when-rows-unmount","Focus Management When Rows Unmount",[324,1227,1228,1229,1231,1232,424,1234,1236],{},"The hardest virtualization problem is focus. If the focused row scrolls out of the window, react-window unmounts it, and focus silently falls to ",[328,1230,411],{},"—a ",[328,1233,374],{},[328,1235,369],{}," failure. There are two robust strategies:",[351,1238,1239,1257],{},[354,1240,1241,1246,1247,1249,1250,1253,1254,1256],{},[347,1242,1243,1245],{},[328,1244,1218],{}," (recommended for grids)."," Keep DOM focus on the grid container at all times and track the \"active\" cell with ",[328,1248,1218],{}," pointing at the active cell's ",[328,1251,1252],{},"id",". Because real focus never leaves the container, unmounting a row cannot strip it. On arrow-key navigation, scroll the target row into view first, then update ",[328,1255,1218],{}," to its id.",[354,1258,1259,1265,1266,1269,1270,1273,1274,1277,1278,1281],{},[347,1260,1261,1262,1264],{},"Roving ",[328,1263,1214],{}," with scroll-into-view."," Give only the active cell ",[328,1267,1268],{},"tabIndex={0}"," and the rest ",[328,1271,1272],{},"tabIndex={-1}",". Before moving focus to a row that may be outside the window, call the virtualizer's scroll API (",[328,1275,1276],{},"listRef.current.scrollToItem(index)",") so the target is mounted, then ",[328,1279,1280],{},".focus()"," it on the next frame.",[490,1283,1285],{"className":492,"code":1284,"language":494,"meta":495,"style":495},"\u002F\u002F Roving-tabindex move that survives windowing: scroll first, then focus.\nfunction moveTo(index: number) {\n  setActiveIndex(index);\n  listRef.current?.scrollToItem(index, 'smart'); \u002F\u002F ensure the row is mounted\n  requestAnimationFrame(() => {\n    const el = document.getElementById(`row-${index}`);\n    el?.focus(); \u002F\u002F safe now that the row exists in the DOM\n  });\n}\n",[328,1286,1287,1292,1313,1321,1341,1353,1382,1396,1401],{"__ignoreMap":495},[499,1288,1289],{"class":501,"line":502},[499,1290,1291],{"class":633},"\u002F\u002F Roving-tabindex move that survives windowing: scroll first, then focus.\n",[499,1293,1294,1297,1300,1303,1305,1307,1310],{"class":501,"line":513},[499,1295,1296],{"class":516},"function",[499,1298,1299],{"class":591}," moveTo",[499,1301,1302],{"class":509},"(",[499,1304,87],{"class":598},[499,1306,605],{"class":516},[499,1308,1309],{"class":550}," number",[499,1311,1312],{"class":509},") {\n",[499,1314,1315,1318],{"class":501,"line":537},[499,1316,1317],{"class":591},"  setActiveIndex",[499,1319,1320],{"class":509},"(index);\n",[499,1322,1323,1326,1329,1332,1335,1338],{"class":501,"line":544},[499,1324,1325],{"class":509},"  listRef.current?.",[499,1327,1328],{"class":591},"scrollToItem",[499,1330,1331],{"class":509},"(index, ",[499,1333,1334],{"class":505},"'smart'",[499,1336,1337],{"class":509},"); ",[499,1339,1340],{"class":633},"\u002F\u002F ensure the row is mounted\n",[499,1342,1343,1346,1349,1351],{"class":501,"line":577},[499,1344,1345],{"class":591},"  requestAnimationFrame",[499,1347,1348],{"class":509},"(() ",[499,1350,805],{"class":516},[499,1352,966],{"class":509},[499,1354,1355,1358,1361,1363,1366,1369,1371,1374,1376,1379],{"class":501,"line":582},[499,1356,1357],{"class":516},"    const",[499,1359,1360],{"class":550}," el",[499,1362,554],{"class":516},[499,1364,1365],{"class":509}," document.",[499,1367,1368],{"class":591},"getElementById",[499,1370,1302],{"class":509},[499,1372,1373],{"class":505},"`row-${",[499,1375,87],{"class":509},[499,1377,1378],{"class":505},"}`",[499,1380,1381],{"class":509},");\n",[499,1383,1384,1387,1390,1393],{"class":501,"line":621},[499,1385,1386],{"class":509},"    el?.",[499,1388,1389],{"class":591},"focus",[499,1391,1392],{"class":509},"(); ",[499,1394,1395],{"class":633},"\u002F\u002F safe now that the row exists in the DOM\n",[499,1397,1398],{"class":501,"line":630},[499,1399,1400],{"class":509},"  });\n",[499,1402,1403],{"class":501,"line":637},[499,1404,696],{"class":509},[324,1406,1407,1408,1410],{},"The non-negotiable rule: never call ",[328,1409,1280],{}," on a row index that the virtualizer has not yet mounted. Scroll it into view first, wait one frame, then focus. Skipping the scroll is the single most common cause of \"focus jumps to the top of the page\" bugs in virtualized lists.",[376,1412],{},[379,1414,1416],{"id":1415},"dont-virtualize-when-a-plain-table-suffices","Don't Virtualize When a Plain Table Suffices",[324,1418,1419,1420,1422],{},"Virtualization is an optimization with a real accessibility cost, so apply it only when the dataset actually demands it. A few hundred rows render and scroll fine in a plain semantic ",[328,1421,447],{},", which is more robust, fully navigable by assistive technology with zero extra ARIA, and far less code. Reaching for react-window on a 200-row table trades correctness for a performance win you do not need.",[324,1424,1425,1426,1428,1429,1433],{},"A reasonable rule of thumb: keep a plain ",[328,1427,447],{}," until row counts climb into the low thousands or you measure a genuine scroll\u002Frender problem. When you do virtualize, prefer paginating the data instead where the UX allows—pagination, covered in ",[336,1430,1432],{"href":1431},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002F","accessible pagination for React data tables",", keeps a small, fully-rendered, fully-semantic table on screen and sidesteps the entire windowing problem. Virtualize only when users genuinely need to scroll one continuous very long list.",[324,1435,1436,1437,713],{},"For the broader machinery—custom hooks for focus tracking and scroll-into-view across recycled DOM—see ",[336,1438,181],{"href":1439},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F",[376,1441],{},[379,1443,1445],{"id":1444},"how-to-verify","How to Verify",[351,1447,1448,1466,1472,1484],{},[354,1449,1450,1453,1454,1456,1457,424,1459,1461,1462,1465],{},[347,1451,1452],{},"Automated (axe-core \u002F jest-axe):"," Assert no violations and check for invalid grid structure. axe flags a ",[328,1455,342],{}," missing required structure, but it cannot verify that ",[328,1458,423],{},[328,1460,427],{}," carry the ",[417,1463,1464],{},"true"," dataset values—that is a manual or unit-test check.",[354,1467,1468,1471],{},[347,1469,1470],{},"Size and position (screen reader):"," With NVDA + Firefox or VoiceOver + Safari, navigate to a row deep in the list. Confirm the screen reader announces the correct total (\"of 50,001\") and the correct absolute position (\"row 8,400\"), not the windowed count.",[354,1473,1474,1477,1478,1480,1481,1483],{},[347,1475,1476],{},"Focus across recycling (keyboard):"," Move focus to a row, then scroll far enough that the row unmounts. Confirm focus is preserved—either still on the grid container via ",[328,1479,1218],{}," or moved deliberately—never dropped to ",[328,1482,411],{},". Arrow from the top to a row well outside the initial window and confirm focus follows.",[354,1485,1486,1489,1490,1492,1493,1495,1496,1499],{},[347,1487,1488],{},"Unit test the offsets:"," Assert that the first data row has ",[328,1491,1205],{}," (header is 1) and that ",[328,1494,423],{}," equals ",[328,1497,1498],{},"rows.length + 1",". Off-by-one errors here are common and silent.",[1501,1502,1503],"blockquote",{},[324,1504,1505,1508,1509,1511,1512,1514],{},[347,1506,1507],{},"Testing Hook:"," Render the virtual grid with a known large dataset, query the last mounted row, and assert its ",[328,1510,427],{}," matches its true dataset position, and that the grid's ",[328,1513,423],{}," equals the full total plus the header.",[376,1516],{},[379,1518,1520],{"id":1519},"common-a11y-mistakes","Common a11y Mistakes",[351,1522,1523,1533,1541,1549,1558,1567],{},[354,1524,1525,1532],{},[347,1526,1527,1528,86,1530],{},"Virtualizing without ",[328,1529,423],{},[328,1531,427],{},", so assistive technology reports only the windowed slice and the true dataset size is lost.",[354,1534,1535,1540],{},[347,1536,1537,1538],{},"Off-by-one ",[328,1539,427],{}," that forgets the header occupies index 1, so every announced position is wrong.",[354,1542,1543,1548],{},[347,1544,1545,1546],{},"Letting focus drop to ",[328,1547,411],{}," when the focused row unmounts during scroll.",[354,1550,1551,1557],{},[347,1552,1553,1554,1556],{},"Calling ",[328,1555,1280],{}," on a row the virtualizer has not mounted yet"," instead of scrolling it into view first.",[354,1559,1560,1566],{},[347,1561,1562,1563,1565],{},"Adopting ",[328,1564,342],{}," but leaving every cell individually tabbable",", mixing the grid keyboard model with normal tabbing.",[354,1568,1569,1572,1573,1575],{},[347,1570,1571],{},"Virtualizing a few-hundred-row table"," that a plain semantic ",[328,1574,447],{},"—or simple pagination—would handle with no accessibility cost.",[376,1577],{},[379,1579,1581],{"id":1580},"frequently-asked-questions","Frequently Asked Questions",[324,1583,1584,1587,1588,424,1590,713],{},[347,1585,1586],{},"Why does react-window break my table for screen readers?","\nWindowing renders only the rows near the viewport, so the DOM—and therefore the accessibility tree—holds a small slice of the data. A screen reader counts the rows it can see and reports that count, so a 50,000-row dataset announces as a dozen rows, and individual rows have no sense of their true position. You must restore the real dimensions with ",[328,1589,423],{},[328,1591,427],{},[324,1593,1594,1600,1602,1603,1605,1606,713],{},[347,1595,1596,1597,1599],{},"What is the difference between ",[328,1598,423],{}," and the number of rows in the DOM?",[328,1601,423],{}," declares the total number of rows in the full dataset, including the header row, regardless of how many are currently rendered. The DOM holds only the windowed slice. Reporting the true total via ",[328,1604,423],{}," is what lets assistive technology tell the user the table has 50,001 rows even though only a handful exist in the DOM, satisfying ",[328,1607,364],{},[324,1609,1610,1613,1614,1616,1617,1619,1620,1622,1623,1625,1626,424,1628,713],{},[347,1611,1612],{},"How do I keep keyboard focus from disappearing when a row scrolls out of view?","\nEither keep real focus on the grid container and track the active cell with ",[328,1615,1218],{},", which cannot be lost because focus never leaves the container, or use roving ",[328,1618,1214],{}," and always scroll the target row into view with the virtualizer's scroll API before calling ",[328,1621,1280],{}," on the next frame. Both prevent the browser from dropping focus to ",[328,1624,411],{}," when a focused row unmounts, satisfying ",[328,1627,374],{},[328,1629,369],{},[324,1631,1632,1635,1636,1638],{},[347,1633,1634],{},"When should I avoid virtualization for accessibility reasons?","\nWhen the dataset is small enough to render fully without a measured performance problem—roughly up to the low thousands of rows. A plain semantic ",[328,1637,447],{}," is fully accessible with no extra ARIA and far less code. Where the UX allows, prefer pagination over virtualization, since it keeps a small, fully-rendered, fully-semantic table on screen and avoids the windowing accessibility problems entirely.",[376,1640],{},[379,1642,1644],{"id":1643},"related-guides","Related guides",[351,1646,1647,1651,1655],{},[354,1648,1649],{},[336,1650,109],{"href":338},[354,1652,1653],{},[336,1654,115],{"href":1431},[354,1656,1657],{},[336,1658,181],{"href":1439},[952,1660,1661],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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);}",{"title":495,"searchDepth":513,"depth":513,"links":1663},[1664,1665,1667,1668,1669,1670,1671,1672],{"id":381,"depth":513,"text":382},{"id":433,"depth":513,"text":1666},"Restoring True Size With aria-rowcount and aria-rowindex",{"id":1224,"depth":513,"text":1225},{"id":1415,"depth":513,"text":1416},{"id":1444,"depth":513,"text":1445},{"id":1519,"depth":513,"text":1520},{"id":1580,"depth":513,"text":1581},{"id":1643,"depth":513,"text":1644},null,"Keep windowed lists and tables accessible—preserve row semantics with aria-rowcount and aria-rowindex, manage focus across recycled DOM, and avoid breaking screen readers.","md",{},false,{"title":127,"description":1674},"IMxfA0gjiaq1pkD3GgajltyD-3CbktYFK-IuHBkjIpA",[1681,1720,1721,1784],{"title":5,"path":6,"stem":7,"children":1682},[1683,1684,1687,1690,1696,1702,1711,1717],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":1685},[1686],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":1688},[1689],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":1691},[1692,1693],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":1694},[1695],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":1697},[1698,1699],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":1700},[1701],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":1703},[1704,1705,1708],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":1706},[1707],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":1709},[1710],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":1712},[1713,1714],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":1715},[1716],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":1718},[1719],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":1722},[1723,1724,1730,1742,1754,1757,1766,1778],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":1725},[1726,1727],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":1728},[1729],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":1731},[1732,1733,1736,1739],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":1734},[1735],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":1737},[1738],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":1740},[1741],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":1743},[1744,1745,1748,1751],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":1746},[1747],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":1749},[1750],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":1752},[1753],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":1755},[1756],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":1758},[1759,1760,1763],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":1761},[1762],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":1764},[1765],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":1767},[1768,1769,1772,1775],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":1770},[1771],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":1773},[1774],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":1776},[1777],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":1779},[1780,1781],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":1782},[1783],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":1785},[1786,1787,1796,1805,1814,1823],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":1788},[1789,1790,1793],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":1791},[1792],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":1794},[1795],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":1797},[1798,1799,1802],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":1800},[1801],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":1803},[1804],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":1806},[1807,1808,1811],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":1809},[1810],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":1812},[1813],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":1815},[1816,1817,1820],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":1818},[1819],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":1821},[1822],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":1824},[1825,1826,1829],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":1827},[1828],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":1830},[1831],{"title":309,"path":310,"stem":311},1781785524109]