[{"data":1,"prerenderedAt":3061},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002F":314,"content-navigation":2909},[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":109,"body":316,"date":2902,"description":2903,"extension":2904,"image":2902,"meta":2905,"modifiedAt":2902,"navigation":1536,"noindex":2906,"path":110,"publishedAt":2902,"seo":2907,"stem":111,"updatedAt":2902,"__hash__":2908},"content\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Findex.md",{"type":317,"value":318,"toc":2886},"minimark",[319,323,332,363,369,395,400,423,426,431,434,611,620,622,633,647,671,1050,1096,1119,1121,1128,1136,1150,1165,1427,1441,1453,1455,1462,1465,1503,1902,1909,1922,1924,1928,1935,2186,2195,2202,2204,2216,2234,2246,2268,2278,2280,2284,2295,2653,2663,2673,2675,2679,2744,2746,2750,2768,2796,2819,2838,2850,2852,2856,2882],[320,321,109],"h1",{"id":322},"accessible-data-tables-grids-in-react",[324,325,326,327,331],"p",{},"Data tables are where accessibility most often quietly breaks in React applications. A table that looks correct on screen can be unintelligible to a screen reader user the moment a ",[328,329,330],"code",{},"\u003Cdiv>"," grid replaces native markup, a header loses its association with its column, or a sort control swaps a row order with no announcement. Tabular data carries meaning in its structure—the relationship between a cell and its row and column headers is the information. When that structure is destroyed, the data is gone for anyone who cannot see the visual grid.",[324,333,334,335,339,340,343,344,347,348,352,353,357,358,362],{},"This guide sits within the broader ",[336,337,94],"a",{"href":338},"\u002Freact-nextjs-accessibility-patterns\u002F"," and shows how to keep tabular semantics intact through the features real applications demand: sorting, selection, pagination, and virtualization. It pairs directly with ",[336,341,133],{"href":342},"\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002F"," for live-region behavior and with ",[336,345,97],{"href":346},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002F"," when you evaluate prebuilt grids. Deep dives live in the three child guides: ",[336,349,351],{"href":350},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002F","building a sortable accessible data table",", ",[336,354,356],{"href":355},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002F","accessible pagination",", and ",[336,359,361],{"href":360},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002F","virtualizing long lists accessibly",".",[324,364,365],{},[366,367,368],"strong",{},"Mapped WCAG Success Criteria:",[370,371,372,379,384,389],"ul",{},[373,374,375,378],"li",{},[328,376,377],{},"1.3.1 Info and Relationships"," (Level A)",[373,380,381,378],{},[328,382,383],{},"4.1.2 Name, Role, Value",[373,385,386,378],{},[328,387,388],{},"2.1.1 Keyboard",[373,390,391,394],{},[328,392,393],{},"4.1.3 Status Messages"," (Level AA)",[324,396,397],{},[366,398,399],{},"Core Implementation Focus:",[370,401,402,405,417,420],{},[373,403,404],{},"Preserving table semantics across CSS layout and component abstraction",[373,406,407,408,352,411,357,414],{},"Header-to-cell association with ",[328,409,410],{},"\u003Ccaption>",[328,412,413],{},"scope",[328,415,416],{},"headers",[373,418,419],{},"Sortable columns that announce their new state",[373,421,422],{},"Selection, pagination, and virtualization without dropping the accessibility tree",[424,425],"hr",{},[427,428,430],"h2",{"id":429},"anatomy-of-an-accessible-table","Anatomy of an Accessible Table",[324,432,433],{},"Before any framework code, it helps to see exactly what a screen reader derives from semantic table markup. Each structural element maps to something the assistive technology announces as the user navigates cell by cell.",[435,436,443,444,443,448,443,443,452,443,463,443,443,471,443,477,443,483,443,487,443,491,443,497,443,500,443,503,443,443,506,443,509,443,513,443,517,443,519,443,523,443,525,443,443,528,443,531,443,534,443,536,443,539,443,541,443,443,544,443,550,443,443,566,443,570,443,575,443,580,443,584,443,588,443,591,443,595,443,599,443,603,443,607],"svg",{"role":437,"ariaLabelledBy":438,"viewBox":441,"style":442},"img",[439,440],"tblTitle","tblDesc","0 0 760 400","width:100%;height:auto;max-width:760px","\n  ",[445,446,447],"title",{"id":439},"How table markup maps to screen reader announcements",[449,450,451],"desc",{"id":440},"A small data table on the left with a caption, two column headers, a row header, and data cells. Arrows connect each markup element to a panel on the right showing what a screen reader announces: the caption names the table, column and row headers are spoken together with each data cell so the user always knows which column and row a value belongs to.",[453,454],"rect",{"style":455,"x":456,"y":457,"width":458,"height":459,"rx":460,"fill":461,"stroke":462},"stroke-width:2","20","60","340","280","8","var(--surface)","var(--primary-strong)",[464,465,470],"text",{"style":466,"x":467,"y":468,"fill":469},"font-size:14px;text-anchor:middle","190","44","var(--text)","\u003Ccaption> Q3 revenue by region \u003C\u002Fcaption>",[453,472],{"style":455,"x":456,"y":457,"width":473,"height":474,"fill":475,"stroke":476},"120","50","var(--primary-soft)","currentColor",[464,478,482],{"style":479,"x":480,"y":481,"fill":469},"font-size:12px;text-anchor:middle","80","90","th scope=col",[453,484],{"style":455,"x":485,"y":457,"width":486,"height":474,"fill":475,"stroke":476},"140","110",[464,488,482],{"style":479,"x":489,"y":490,"fill":469},"195","82",[464,492,496],{"style":493,"x":489,"y":494,"fill":495},"font-size:11px;text-anchor:middle","98","var(--muted)","Revenue",[453,498],{"style":455,"x":499,"y":457,"width":486,"height":474,"fill":475,"stroke":476},"250",[464,501,482],{"style":479,"x":502,"y":490,"fill":469},"305",[464,504,505],{"style":493,"x":502,"y":494,"fill":495},"Growth",[453,507],{"style":455,"x":456,"y":486,"width":473,"height":474,"fill":508,"stroke":476},"var(--surface-muted)",[464,510,512],{"style":479,"x":480,"y":511,"fill":469},"135","th scope=row",[464,514,516],{"style":493,"x":480,"y":515,"fill":495},"151","EMEA",[453,518],{"style":455,"x":485,"y":486,"width":486,"height":474,"fill":461,"stroke":476},[464,520,522],{"style":521,"x":489,"y":485,"fill":469},"font-size:13px;text-anchor:middle","$4.2M",[453,524],{"style":455,"x":499,"y":486,"width":486,"height":474,"fill":461,"stroke":476},[464,526,527],{"style":521,"x":502,"y":485,"fill":469},"+8%",[453,529],{"style":455,"x":456,"y":530,"width":473,"height":474,"fill":508,"stroke":476},"160",[464,532,533],{"style":493,"x":480,"y":467,"fill":495},"APAC",[453,535],{"style":455,"x":485,"y":530,"width":486,"height":474,"fill":461,"stroke":476},[464,537,538],{"style":521,"x":489,"y":467,"fill":469},"$3.1M",[453,540],{"style":455,"x":499,"y":530,"width":486,"height":474,"fill":461,"stroke":476},[464,542,543],{"style":521,"x":502,"y":467,"fill":469},"+15%",[545,546],"line",{"style":547,"x1":548,"y1":511,"x2":549,"y2":511,"stroke":476},"stroke-width:2;marker-end:url(#tblArrow)","360","430",[551,552,553,554,443],"defs",{},"\n    ",[555,556,561,562,553],"marker",{"id":557,"markerWidth":558,"markerHeight":558,"refX":460,"refY":559,"orient":560},"tblArrow","10","3","auto","\n      ",[563,564],"path",{"d":565,"fill":476},"M0,0 L8,3 L0,6 Z",[453,567],{"style":455,"x":568,"y":457,"width":569,"height":459,"rx":460,"fill":475,"stroke":462},"436","304",[464,571,574],{"style":521,"x":572,"y":573,"fill":462},"588","86","Screen reader announces",[464,576,579],{"style":577,"x":578,"y":473,"fill":469},"font-size:12px","452","On enter: \"Q3 revenue by region,",[464,581,583],{"style":577,"x":578,"y":582,"fill":469},"138","table, 3 columns, 3 rows\"",[464,585,587],{"style":577,"x":578,"y":586,"fill":469},"172","Cell $4.2M: \"Revenue, EMEA,",[464,589,590],{"style":577,"x":578,"y":467,"fill":469},"$4.2M\"",[464,592,594],{"style":577,"x":578,"y":593,"fill":495},"224","Column header + row header are",[464,596,598],{"style":577,"x":578,"y":597,"fill":495},"242","spoken with every cell, so the",[464,600,602],{"style":577,"x":578,"y":601,"fill":495},"260","value never loses its context.",[464,604,606],{"style":577,"x":578,"y":605,"fill":469},"300","Remove the markup and all that",[464,608,610],{"style":577,"x":578,"y":609,"fill":469},"318","context disappears.",[324,612,613,614,352,616,619],{},"The takeaway: the screen reader's announcement is reconstructed entirely from ",[328,615,410],{},[328,617,618],{},"\u003Cth scope>",", and the row\u002Fcolumn geometry of native table elements. Everything in the rest of this guide protects those relationships.",[424,621],{},[427,623,625,626,629,630,632],{"id":624},"semantic-table-vs-div-grids","Semantic ",[328,627,628],{},"\u003Ctable>"," vs ",[328,631,330],{}," Grids",[324,634,635,636,638,639,642,643,646],{},"The single most common table accessibility failure is rebuilding a table out of ",[328,637,330],{},"s for styling flexibility. A ",[328,640,641],{},"\u003Cdiv className=\"row\">"," containing ",[328,644,645],{},"\u003Cdiv className=\"cell\">"," elements renders identically on screen but exposes nothing to assistive technology: no table role, no row or column count, no header associations. Screen reader table-navigation commands (jump to next cell, read column header) stop working entirely because there is no table to navigate.",[324,648,649,650,652,653,352,655,352,658,352,661,352,664,357,667,670],{},"Native elements give you the relationships defined by ",[328,651,377],{}," for free. Reach for ",[328,654,628],{},[328,656,657],{},"\u003Cthead>",[328,659,660],{},"\u003Ctbody>",[328,662,663],{},"\u003Ctr>",[328,665,666],{},"\u003Cth>",[328,668,669],{},"\u003Ctd>"," first.",[672,673,678],"pre",{"className":674,"code":675,"language":676,"meta":677,"style":677},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F A native semantic table — every relationship is implicit and free.\nfunction RevenueTable({ rows }: { rows: RegionRow[] }) {\n  return (\n    \u003Ctable>\n      {\u002F* caption names the table for AT and is the first thing announced *\u002F}\n      \u003Ccaption>Q3 revenue by region\u003C\u002Fcaption>\n      \u003Cthead>\n        \u003Ctr>\n          {\u002F* scope=\"col\" associates each header with its entire column *\u002F}\n          \u003Cth scope=\"col\">Region\u003C\u002Fth>\n          \u003Cth scope=\"col\">Revenue\u003C\u002Fth>\n          \u003Cth scope=\"col\">Growth\u003C\u002Fth>\n        \u003C\u002Ftr>\n      \u003C\u002Fthead>\n      \u003Ctbody>\n        {rows.map((row) => (\n          \u003Ctr key={row.id}>\n            {\u002F* scope=\"row\" makes the region label a header for the data cells *\u002F}\n            \u003Cth scope=\"row\">{row.region}\u003C\u002Fth>\n            \u003Ctd>{row.revenue}\u003C\u002Ftd>\n            \u003Ctd>{row.growth}\u003C\u002Ftd>\n          \u003C\u002Ftr>\n        ))}\n      \u003C\u002Ftbody>\n    \u003C\u002Ftable>\n  );\n}\n","tsx","",[328,679,680,688,726,735,748,760,776,786,797,808,834,854,874,884,894,904,927,942,953,975,990,1004,1014,1020,1029,1039,1045],{"__ignoreMap":677},[681,682,684],"span",{"class":545,"line":683},1,[681,685,687],{"class":686},"sJ8bj","\u002F\u002F A native semantic table — every relationship is implicit and free.\n",[681,689,691,695,699,703,707,710,713,716,718,720,723],{"class":545,"line":690},2,[681,692,694],{"class":693},"szBVR","function",[681,696,698],{"class":697},"sScJk"," RevenueTable",[681,700,702],{"class":701},"sVt8B","({ ",[681,704,706],{"class":705},"s4XuR","rows",[681,708,709],{"class":701}," }",[681,711,712],{"class":693},":",[681,714,715],{"class":701}," { ",[681,717,706],{"class":705},[681,719,712],{"class":693},[681,721,722],{"class":697}," RegionRow",[681,724,725],{"class":701},"[] }) {\n",[681,727,729,732],{"class":545,"line":728},3,[681,730,731],{"class":693},"  return",[681,733,734],{"class":701}," (\n",[681,736,738,741,745],{"class":545,"line":737},4,[681,739,740],{"class":701},"    \u003C",[681,742,744],{"class":743},"s9eBZ","table",[681,746,747],{"class":701},">\n",[681,749,751,754,757],{"class":545,"line":750},5,[681,752,753],{"class":701},"      {",[681,755,756],{"class":686},"\u002F* caption names the table for AT and is the first thing announced *\u002F",[681,758,759],{"class":701},"}\n",[681,761,763,766,769,772,774],{"class":545,"line":762},6,[681,764,765],{"class":701},"      \u003C",[681,767,768],{"class":743},"caption",[681,770,771],{"class":701},">Q3 revenue by region\u003C\u002F",[681,773,768],{"class":743},[681,775,747],{"class":701},[681,777,779,781,784],{"class":545,"line":778},7,[681,780,765],{"class":701},[681,782,783],{"class":743},"thead",[681,785,747],{"class":701},[681,787,789,792,795],{"class":545,"line":788},8,[681,790,791],{"class":701},"        \u003C",[681,793,794],{"class":743},"tr",[681,796,747],{"class":701},[681,798,800,803,806],{"class":545,"line":799},9,[681,801,802],{"class":701},"          {",[681,804,805],{"class":686},"\u002F* scope=\"col\" associates each header with its entire column *\u002F",[681,807,759],{"class":701},[681,809,811,814,817,820,823,827,830,832],{"class":545,"line":810},10,[681,812,813],{"class":701},"          \u003C",[681,815,816],{"class":743},"th",[681,818,819],{"class":697}," scope",[681,821,822],{"class":693},"=",[681,824,826],{"class":825},"sZZnC","\"col\"",[681,828,829],{"class":701},">Region\u003C\u002F",[681,831,816],{"class":743},[681,833,747],{"class":701},[681,835,837,839,841,843,845,847,850,852],{"class":545,"line":836},11,[681,838,813],{"class":701},[681,840,816],{"class":743},[681,842,819],{"class":697},[681,844,822],{"class":693},[681,846,826],{"class":825},[681,848,849],{"class":701},">Revenue\u003C\u002F",[681,851,816],{"class":743},[681,853,747],{"class":701},[681,855,857,859,861,863,865,867,870,872],{"class":545,"line":856},12,[681,858,813],{"class":701},[681,860,816],{"class":743},[681,862,819],{"class":697},[681,864,822],{"class":693},[681,866,826],{"class":825},[681,868,869],{"class":701},">Growth\u003C\u002F",[681,871,816],{"class":743},[681,873,747],{"class":701},[681,875,877,880,882],{"class":545,"line":876},13,[681,878,879],{"class":701},"        \u003C\u002F",[681,881,794],{"class":743},[681,883,747],{"class":701},[681,885,887,890,892],{"class":545,"line":886},14,[681,888,889],{"class":701},"      \u003C\u002F",[681,891,783],{"class":743},[681,893,747],{"class":701},[681,895,897,899,902],{"class":545,"line":896},15,[681,898,765],{"class":701},[681,900,901],{"class":743},"tbody",[681,903,747],{"class":701},[681,905,907,910,913,916,919,922,925],{"class":545,"line":906},16,[681,908,909],{"class":701},"        {rows.",[681,911,912],{"class":697},"map",[681,914,915],{"class":701},"((",[681,917,918],{"class":705},"row",[681,920,921],{"class":701},") ",[681,923,924],{"class":693},"=>",[681,926,734],{"class":701},[681,928,930,932,934,937,939],{"class":545,"line":929},17,[681,931,813],{"class":701},[681,933,794],{"class":743},[681,935,936],{"class":697}," key",[681,938,822],{"class":693},[681,940,941],{"class":701},"{row.id}>\n",[681,943,945,948,951],{"class":545,"line":944},18,[681,946,947],{"class":701},"            {",[681,949,950],{"class":686},"\u002F* scope=\"row\" makes the region label a header for the data cells *\u002F",[681,952,759],{"class":701},[681,954,956,959,961,963,965,968,971,973],{"class":545,"line":955},19,[681,957,958],{"class":701},"            \u003C",[681,960,816],{"class":743},[681,962,819],{"class":697},[681,964,822],{"class":693},[681,966,967],{"class":825},"\"row\"",[681,969,970],{"class":701},">{row.region}\u003C\u002F",[681,972,816],{"class":743},[681,974,747],{"class":701},[681,976,978,980,983,986,988],{"class":545,"line":977},20,[681,979,958],{"class":701},[681,981,982],{"class":743},"td",[681,984,985],{"class":701},">{row.revenue}\u003C\u002F",[681,987,982],{"class":743},[681,989,747],{"class":701},[681,991,993,995,997,1000,1002],{"class":545,"line":992},21,[681,994,958],{"class":701},[681,996,982],{"class":743},[681,998,999],{"class":701},">{row.growth}\u003C\u002F",[681,1001,982],{"class":743},[681,1003,747],{"class":701},[681,1005,1007,1010,1012],{"class":545,"line":1006},22,[681,1008,1009],{"class":701},"          \u003C\u002F",[681,1011,794],{"class":743},[681,1013,747],{"class":701},[681,1015,1017],{"class":545,"line":1016},23,[681,1018,1019],{"class":701},"        ))}\n",[681,1021,1023,1025,1027],{"class":545,"line":1022},24,[681,1024,889],{"class":701},[681,1026,901],{"class":743},[681,1028,747],{"class":701},[681,1030,1032,1035,1037],{"class":545,"line":1031},25,[681,1033,1034],{"class":701},"    \u003C\u002F",[681,1036,744],{"class":743},[681,1038,747],{"class":701},[681,1040,1042],{"class":545,"line":1041},26,[681,1043,1044],{"class":701},"  );\n",[681,1046,1048],{"class":545,"line":1047},27,[681,1049,759],{"class":701},[324,1051,1052,1053,1055,1056,1059,1060,1063,1064,352,1066,1068,1069,1071,1072,1075,1076,1079,1080,1082,1083,352,1086,352,1089,357,1092,1095],{},"A subtle trap: even a native ",[328,1054,628],{}," can lose its semantics through CSS. Setting ",[328,1057,1058],{},"display: flex"," or ",[328,1061,1062],{},"display: grid"," on a ",[328,1065,628],{},[328,1067,663],{},", or ",[328,1070,669],{}," can strip the implicit table roles in some browsers, because the computed ",[328,1073,1074],{},"display"," value drives the accessibility role. If you need CSS Grid layout for a complex responsive table, either keep the table semantics and lay out with ",[328,1077,1078],{},"grid-template-columns"," on the ",[328,1081,628],{}," only after testing in a screen reader, or restore roles explicitly with ",[328,1084,1085],{},"role=\"table\"",[328,1087,1088],{},"role=\"row\"",[328,1090,1091],{},"role=\"columnheader\"",[328,1093,1094],{},"role=\"cell\"",". Prefer not breaking it in the first place.",[1097,1098,1099],"blockquote",{},[324,1100,1101,1104,1105,1108,1109,1112,1113,1115,1116,1118],{},[366,1102,1103],{},"Testing Hook:"," Run the table through your screen reader's table-navigation commands (NVDA: ",[328,1106,1107],{},"Ctrl+Alt+Arrow","; VoiceOver: ",[328,1110,1111],{},"Ctrl+Option+Arrow","). If those commands do nothing, the table role has been lost—almost always to a ",[328,1114,1074],{}," override or a ",[328,1117,330],{}," grid.",[424,1120],{},[427,1122,1124,1125,1127],{"id":1123},"caption-scope-and-header-associations","Caption, ",[328,1126,413],{},", and Header Associations",[324,1129,1130,1132,1133,1135],{},[328,1131,377],{}," is satisfied when every data cell is programmatically associated with the headers that describe it. For the overwhelming majority of tables, ",[328,1134,413],{}," does this:",[370,1137,1138,1144],{},[373,1139,1140,1143],{},[328,1141,1142],{},"\u003Cth scope=\"col\">"," associates the header with all cells beneath it in the column.",[373,1145,1146,1149],{},[328,1147,1148],{},"\u003Cth scope=\"row\">"," associates the header with all cells in its row—ideal for the first column that names each record.",[324,1151,1152,1153,1155,1156,86,1159,1161,1162,1164],{},"For irregular tables with merged cells or multiple header layers, ",[328,1154,413],{}," is not expressive enough. Use explicit ",[328,1157,1158],{},"id",[328,1160,416],{}," wiring, where each ",[328,1163,669],{}," lists the IDs of every header that applies to it.",[672,1166,1168],{"className":674,"code":1167,"language":676,"meta":677,"style":677},"\u002F\u002F Complex header: explicit id\u002Fheaders wiring for a cell governed by two headers.\n\u003Ctable>\n  \u003Ccaption>Quarterly headcount by department and location\u003C\u002Fcaption>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth scope=\"col\" id=\"dept\">Department\u003C\u002Fth>\n      \u003Cth scope=\"col\" id=\"ny\">New York\u003C\u002Fth>\n      \u003Cth scope=\"col\" id=\"ldn\">London\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Cth scope=\"row\" id=\"eng\">Engineering\u003C\u002Fth>\n      {\u002F* headers ties this cell to both its column and row headers *\u002F}\n      \u003Ctd headers=\"eng ny\">48\u003C\u002Ftd>\n      \u003Ctd headers=\"eng ldn\">31\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n",[328,1169,1170,1175,1184,1198,1206,1214,1241,1267,1293,1301,1310,1318,1326,1352,1361,1382,1402,1410,1418],{"__ignoreMap":677},[681,1171,1172],{"class":545,"line":683},[681,1173,1174],{"class":686},"\u002F\u002F Complex header: explicit id\u002Fheaders wiring for a cell governed by two headers.\n",[681,1176,1177,1180,1182],{"class":545,"line":690},[681,1178,1179],{"class":701},"\u003C",[681,1181,744],{"class":743},[681,1183,747],{"class":701},[681,1185,1186,1189,1191,1194,1196],{"class":545,"line":728},[681,1187,1188],{"class":701},"  \u003C",[681,1190,768],{"class":743},[681,1192,1193],{"class":701},">Quarterly headcount by department and location\u003C\u002F",[681,1195,768],{"class":743},[681,1197,747],{"class":701},[681,1199,1200,1202,1204],{"class":545,"line":737},[681,1201,1188],{"class":701},[681,1203,783],{"class":743},[681,1205,747],{"class":701},[681,1207,1208,1210,1212],{"class":545,"line":750},[681,1209,740],{"class":701},[681,1211,794],{"class":743},[681,1213,747],{"class":701},[681,1215,1216,1218,1220,1222,1224,1226,1229,1231,1234,1237,1239],{"class":545,"line":762},[681,1217,765],{"class":701},[681,1219,816],{"class":743},[681,1221,819],{"class":697},[681,1223,822],{"class":693},[681,1225,826],{"class":825},[681,1227,1228],{"class":697}," id",[681,1230,822],{"class":693},[681,1232,1233],{"class":825},"\"dept\"",[681,1235,1236],{"class":701},">Department\u003C\u002F",[681,1238,816],{"class":743},[681,1240,747],{"class":701},[681,1242,1243,1245,1247,1249,1251,1253,1255,1257,1260,1263,1265],{"class":545,"line":778},[681,1244,765],{"class":701},[681,1246,816],{"class":743},[681,1248,819],{"class":697},[681,1250,822],{"class":693},[681,1252,826],{"class":825},[681,1254,1228],{"class":697},[681,1256,822],{"class":693},[681,1258,1259],{"class":825},"\"ny\"",[681,1261,1262],{"class":701},">New York\u003C\u002F",[681,1264,816],{"class":743},[681,1266,747],{"class":701},[681,1268,1269,1271,1273,1275,1277,1279,1281,1283,1286,1289,1291],{"class":545,"line":788},[681,1270,765],{"class":701},[681,1272,816],{"class":743},[681,1274,819],{"class":697},[681,1276,822],{"class":693},[681,1278,826],{"class":825},[681,1280,1228],{"class":697},[681,1282,822],{"class":693},[681,1284,1285],{"class":825},"\"ldn\"",[681,1287,1288],{"class":701},">London\u003C\u002F",[681,1290,816],{"class":743},[681,1292,747],{"class":701},[681,1294,1295,1297,1299],{"class":545,"line":799},[681,1296,1034],{"class":701},[681,1298,794],{"class":743},[681,1300,747],{"class":701},[681,1302,1303,1306,1308],{"class":545,"line":810},[681,1304,1305],{"class":701},"  \u003C\u002F",[681,1307,783],{"class":743},[681,1309,747],{"class":701},[681,1311,1312,1314,1316],{"class":545,"line":836},[681,1313,1188],{"class":701},[681,1315,901],{"class":743},[681,1317,747],{"class":701},[681,1319,1320,1322,1324],{"class":545,"line":856},[681,1321,740],{"class":701},[681,1323,794],{"class":743},[681,1325,747],{"class":701},[681,1327,1328,1330,1332,1334,1336,1338,1340,1342,1345,1348,1350],{"class":545,"line":876},[681,1329,765],{"class":701},[681,1331,816],{"class":743},[681,1333,819],{"class":697},[681,1335,822],{"class":693},[681,1337,967],{"class":825},[681,1339,1228],{"class":697},[681,1341,822],{"class":693},[681,1343,1344],{"class":825},"\"eng\"",[681,1346,1347],{"class":701},">Engineering\u003C\u002F",[681,1349,816],{"class":743},[681,1351,747],{"class":701},[681,1353,1354,1356,1359],{"class":545,"line":886},[681,1355,753],{"class":701},[681,1357,1358],{"class":686},"\u002F* headers ties this cell to both its column and row headers *\u002F",[681,1360,759],{"class":701},[681,1362,1363,1365,1367,1370,1372,1375,1378,1380],{"class":545,"line":896},[681,1364,765],{"class":701},[681,1366,982],{"class":743},[681,1368,1369],{"class":697}," headers",[681,1371,822],{"class":693},[681,1373,1374],{"class":825},"\"eng ny\"",[681,1376,1377],{"class":701},">48\u003C\u002F",[681,1379,982],{"class":743},[681,1381,747],{"class":701},[681,1383,1384,1386,1388,1390,1392,1395,1398,1400],{"class":545,"line":906},[681,1385,765],{"class":701},[681,1387,982],{"class":743},[681,1389,1369],{"class":697},[681,1391,822],{"class":693},[681,1393,1394],{"class":825},"\"eng ldn\"",[681,1396,1397],{"class":701},">31\u003C\u002F",[681,1399,982],{"class":743},[681,1401,747],{"class":701},[681,1403,1404,1406,1408],{"class":545,"line":929},[681,1405,1034],{"class":701},[681,1407,794],{"class":743},[681,1409,747],{"class":701},[681,1411,1412,1414,1416],{"class":545,"line":944},[681,1413,1305],{"class":701},[681,1415,901],{"class":743},[681,1417,747],{"class":701},[681,1419,1420,1423,1425],{"class":545,"line":955},[681,1421,1422],{"class":701},"\u003C\u002F",[681,1424,744],{"class":743},[681,1426,747],{"class":701},[324,1428,1429,1430,1432,1433,1436,1437,1440],{},"Always provide a ",[328,1431,410],{},"—it is the accessible name of the table and the first thing announced. If the visual design has no room for a caption, keep the element and visually hide it with an ",[328,1434,1435],{},"sr-only"," utility rather than dropping it. Never substitute a nearby ",[328,1438,1439],{},"\u003Ch2>"," for a real caption; the heading is not programmatically tied to the table.",[1097,1442,1443],{},[324,1444,1445,1447,1448,1452],{},[366,1446,1103],{}," axe-core flags missing captions and orphaned data cells, but it cannot verify that the associations are ",[1449,1450,1451],"em",{},"correct",". Manually arrow into a data cell and confirm the screen reader speaks the intended column and row header before the value.",[424,1454],{},[427,1456,1458,1459],{"id":1457},"sortable-columns-with-aria-sort","Sortable Columns With ",[328,1460,1461],{},"aria-sort",[324,1463,1464],{},"Sortable headers are where keyboard and screen reader support most often regress. The accessible pattern has three parts, each tied to a success criterion:",[1466,1467,1468,1480,1497],"ol",{},[373,1469,1470,1471,1474,1475,1477,1478,362],{},"The sort control is a real ",[328,1472,1473],{},"\u003Cbutton>"," inside the ",[328,1476,666],{},", so it is keyboard-focusable and operable per ",[328,1479,388],{},[373,1481,1482,1483,1485,1486,1489,1490,1493,1494,1496],{},"The active column's ",[328,1484,666],{}," carries ",[328,1487,1488],{},"aria-sort=\"ascending\" | \"descending\"","; all other sortable headers carry ",[328,1491,1492],{},"aria-sort=\"none\"",". This satisfies ",[328,1495,383],{}," by exposing the current sort state.",[373,1498,1499,1500,1502],{},"The sort change is announced through a polite live region per ",[328,1501,393],{},", because reordering rows is a visual change a screen reader user would otherwise miss.",[672,1504,1506],{"className":674,"code":1505,"language":676,"meta":677,"style":677},"'use client';\nimport { useState } from 'react';\n\ntype SortDir = 'ascending' | 'descending' | 'none';\n\nfunction SortableHeader({\n  label, columnKey, sort, onSort,\n}: {\n  label: string;\n  columnKey: string;\n  sort: { key: string; dir: SortDir };\n  onSort: (key: string) => void;\n}) {\n  const isActive = sort.key === columnKey;\n  \u002F\u002F aria-sort lives on the th, not the button — AT reads it as a column property\n  const ariaSort: SortDir = isActive ? sort.dir : 'none';\n  return (\n    \u003Cth scope=\"col\" aria-sort={ariaSort}>\n      \u003Cbutton type=\"button\" onClick={() => onSort(columnKey)}>\n        {label}\n        {\u002F* decorative arrow is hidden; aria-sort already conveys direction *\u002F}\n        \u003Cspan aria-hidden=\"true\">\n          {isActive ? (sort.dir === 'ascending' ? ' ▲' : ' ▼') : ''}\n        \u003C\u002Fspan>\n      \u003C\u002Fbutton>\n    \u003C\u002Fth>\n  );\n}\n",[328,1507,1508,1516,1532,1538,1565,1569,1579,1602,1612,1624,1635,1664,1689,1694,1713,1718,1746,1752,1772,1803,1808,1818,1834,1869,1877,1885,1893,1897],{"__ignoreMap":677},[681,1509,1510,1513],{"class":545,"line":683},[681,1511,1512],{"class":825},"'use client'",[681,1514,1515],{"class":701},";\n",[681,1517,1518,1521,1524,1527,1530],{"class":545,"line":690},[681,1519,1520],{"class":693},"import",[681,1522,1523],{"class":701}," { useState } ",[681,1525,1526],{"class":693},"from",[681,1528,1529],{"class":825}," 'react'",[681,1531,1515],{"class":701},[681,1533,1534],{"class":545,"line":728},[681,1535,1537],{"emptyLinePlaceholder":1536},true,"\n",[681,1539,1540,1543,1546,1549,1552,1555,1558,1560,1563],{"class":545,"line":737},[681,1541,1542],{"class":693},"type",[681,1544,1545],{"class":697}," SortDir",[681,1547,1548],{"class":693}," =",[681,1550,1551],{"class":825}," 'ascending'",[681,1553,1554],{"class":693}," |",[681,1556,1557],{"class":825}," 'descending'",[681,1559,1554],{"class":693},[681,1561,1562],{"class":825}," 'none'",[681,1564,1515],{"class":701},[681,1566,1567],{"class":545,"line":750},[681,1568,1537],{"emptyLinePlaceholder":1536},[681,1570,1571,1573,1576],{"class":545,"line":762},[681,1572,694],{"class":693},[681,1574,1575],{"class":697}," SortableHeader",[681,1577,1578],{"class":701},"({\n",[681,1580,1581,1584,1586,1589,1591,1594,1596,1599],{"class":545,"line":778},[681,1582,1583],{"class":705},"  label",[681,1585,352],{"class":701},[681,1587,1588],{"class":705},"columnKey",[681,1590,352],{"class":701},[681,1592,1593],{"class":705},"sort",[681,1595,352],{"class":701},[681,1597,1598],{"class":705},"onSort",[681,1600,1601],{"class":701},",\n",[681,1603,1604,1607,1609],{"class":545,"line":788},[681,1605,1606],{"class":701},"}",[681,1608,712],{"class":693},[681,1610,1611],{"class":701}," {\n",[681,1613,1614,1616,1618,1622],{"class":545,"line":799},[681,1615,1583],{"class":705},[681,1617,712],{"class":693},[681,1619,1621],{"class":1620},"sj4cs"," string",[681,1623,1515],{"class":701},[681,1625,1626,1629,1631,1633],{"class":545,"line":810},[681,1627,1628],{"class":705},"  columnKey",[681,1630,712],{"class":693},[681,1632,1621],{"class":1620},[681,1634,1515],{"class":701},[681,1636,1637,1640,1642,1644,1647,1649,1651,1654,1657,1659,1661],{"class":545,"line":836},[681,1638,1639],{"class":705},"  sort",[681,1641,712],{"class":693},[681,1643,715],{"class":701},[681,1645,1646],{"class":705},"key",[681,1648,712],{"class":693},[681,1650,1621],{"class":1620},[681,1652,1653],{"class":701},"; ",[681,1655,1656],{"class":705},"dir",[681,1658,712],{"class":693},[681,1660,1545],{"class":697},[681,1662,1663],{"class":701}," };\n",[681,1665,1666,1669,1671,1674,1676,1678,1680,1682,1684,1687],{"class":545,"line":856},[681,1667,1668],{"class":697},"  onSort",[681,1670,712],{"class":693},[681,1672,1673],{"class":701}," (",[681,1675,1646],{"class":705},[681,1677,712],{"class":693},[681,1679,1621],{"class":1620},[681,1681,921],{"class":701},[681,1683,924],{"class":693},[681,1685,1686],{"class":1620}," void",[681,1688,1515],{"class":701},[681,1690,1691],{"class":545,"line":876},[681,1692,1693],{"class":701},"}) {\n",[681,1695,1696,1699,1702,1704,1707,1710],{"class":545,"line":886},[681,1697,1698],{"class":693},"  const",[681,1700,1701],{"class":1620}," isActive",[681,1703,1548],{"class":693},[681,1705,1706],{"class":701}," sort.key ",[681,1708,1709],{"class":693},"===",[681,1711,1712],{"class":701}," columnKey;\n",[681,1714,1715],{"class":545,"line":896},[681,1716,1717],{"class":686},"  \u002F\u002F aria-sort lives on the th, not the button — AT reads it as a column property\n",[681,1719,1720,1722,1725,1727,1729,1731,1734,1737,1740,1742,1744],{"class":545,"line":906},[681,1721,1698],{"class":693},[681,1723,1724],{"class":1620}," ariaSort",[681,1726,712],{"class":693},[681,1728,1545],{"class":697},[681,1730,1548],{"class":693},[681,1732,1733],{"class":701}," isActive ",[681,1735,1736],{"class":693},"?",[681,1738,1739],{"class":701}," sort.dir ",[681,1741,712],{"class":693},[681,1743,1562],{"class":825},[681,1745,1515],{"class":701},[681,1747,1748,1750],{"class":545,"line":929},[681,1749,731],{"class":693},[681,1751,734],{"class":701},[681,1753,1754,1756,1758,1760,1762,1764,1767,1769],{"class":545,"line":944},[681,1755,740],{"class":701},[681,1757,816],{"class":743},[681,1759,819],{"class":697},[681,1761,822],{"class":693},[681,1763,826],{"class":825},[681,1765,1766],{"class":697}," aria-sort",[681,1768,822],{"class":693},[681,1770,1771],{"class":701},"{ariaSort}>\n",[681,1773,1774,1776,1779,1782,1784,1787,1790,1792,1795,1797,1800],{"class":545,"line":955},[681,1775,765],{"class":701},[681,1777,1778],{"class":743},"button",[681,1780,1781],{"class":697}," type",[681,1783,822],{"class":693},[681,1785,1786],{"class":825},"\"button\"",[681,1788,1789],{"class":697}," onClick",[681,1791,822],{"class":693},[681,1793,1794],{"class":701},"{() ",[681,1796,924],{"class":693},[681,1798,1799],{"class":697}," onSort",[681,1801,1802],{"class":701},"(columnKey)}>\n",[681,1804,1805],{"class":545,"line":977},[681,1806,1807],{"class":701},"        {label}\n",[681,1809,1810,1813,1816],{"class":545,"line":992},[681,1811,1812],{"class":701},"        {",[681,1814,1815],{"class":686},"\u002F* decorative arrow is hidden; aria-sort already conveys direction *\u002F",[681,1817,759],{"class":701},[681,1819,1820,1822,1824,1827,1829,1832],{"class":545,"line":1006},[681,1821,791],{"class":701},[681,1823,681],{"class":743},[681,1825,1826],{"class":697}," aria-hidden",[681,1828,822],{"class":693},[681,1830,1831],{"class":825},"\"true\"",[681,1833,747],{"class":701},[681,1835,1836,1839,1841,1844,1846,1848,1851,1854,1857,1860,1862,1864,1867],{"class":545,"line":1016},[681,1837,1838],{"class":701},"          {isActive ",[681,1840,1736],{"class":693},[681,1842,1843],{"class":701}," (sort.dir ",[681,1845,1709],{"class":693},[681,1847,1551],{"class":825},[681,1849,1850],{"class":693}," ?",[681,1852,1853],{"class":825}," ' ▲'",[681,1855,1856],{"class":693}," :",[681,1858,1859],{"class":825}," ' ▼'",[681,1861,921],{"class":701},[681,1863,712],{"class":693},[681,1865,1866],{"class":825}," ''",[681,1868,759],{"class":701},[681,1870,1871,1873,1875],{"class":545,"line":1022},[681,1872,879],{"class":701},[681,1874,681],{"class":743},[681,1876,747],{"class":701},[681,1878,1879,1881,1883],{"class":545,"line":1031},[681,1880,889],{"class":701},[681,1882,1778],{"class":743},[681,1884,747],{"class":701},[681,1886,1887,1889,1891],{"class":545,"line":1041},[681,1888,1034],{"class":701},[681,1890,816],{"class":743},[681,1892,747],{"class":701},[681,1894,1895],{"class":545,"line":1047},[681,1896,1044],{"class":701},[681,1898,1900],{"class":545,"line":1899},28,[681,1901,759],{"class":701},[324,1903,1904,1905,1908],{},"The ",[336,1906,1907],{"href":350},"sortable table deep dive"," walks through the full component, including the announcement string (\"Table sorted by Revenue, descending\") and the live region wiring.",[1097,1910,1911],{},[324,1912,1913,1915,1916,1918,1919,1921],{},[366,1914,1103],{}," Tab to a header, press Enter\u002FSpace, and confirm the screen reader announces both the new direction (from ",[328,1917,1461],{},") and the polite message. Verify ",[328,1920,1461],{}," is present on exactly one header at a time.",[424,1923],{},[427,1925,1927],{"id":1926},"row-selection-patterns","Row Selection Patterns",[324,1929,1930,1931,1934],{},"Selectable rows—for bulk actions—need each control to expose its name and checked state. Use a native ",[328,1932,1933],{},"\u003Cinput type=\"checkbox\">"," per row, labeled by the row's header, plus a \"select all\" checkbox in the column header that reflects an indeterminate state when only some rows are selected.",[672,1936,1938],{"className":674,"code":1937,"language":676,"meta":677,"style":677},"function SelectableRows({ rows, selected, toggle }: SelectableProps) {\n  return (\n    \u003Ctbody>\n      {rows.map((row) => (\n        \u003Ctr key={row.id} aria-selected={selected.has(row.id)}>\n          \u003Ctd>\n            \u003Cinput\n              type=\"checkbox\"\n              checked={selected.has(row.id)}\n              onChange={() => toggle(row.id)}\n              \u002F\u002F Name the checkbox by the row it controls — never a bare \"select\"\n              aria-label={`Select ${row.region}`}\n            \u002F>\n          \u003C\u002Ftd>\n          \u003Cth scope=\"row\">{row.region}\u003C\u002Fth>\n          \u003Ctd>{row.revenue}\u003C\u002Ftd>\n        \u003C\u002Ftr>\n      ))}\n    \u003C\u002Ftbody>\n  );\n}\n",[328,1939,1940,1971,1977,1985,2002,2029,2037,2044,2054,2068,2084,2089,2114,2119,2127,2145,2157,2165,2170,2178,2182],{"__ignoreMap":677},[681,1941,1942,1944,1947,1949,1951,1953,1956,1958,1961,1963,1965,1968],{"class":545,"line":683},[681,1943,694],{"class":693},[681,1945,1946],{"class":697}," SelectableRows",[681,1948,702],{"class":701},[681,1950,706],{"class":705},[681,1952,352],{"class":701},[681,1954,1955],{"class":705},"selected",[681,1957,352],{"class":701},[681,1959,1960],{"class":705},"toggle",[681,1962,709],{"class":701},[681,1964,712],{"class":693},[681,1966,1967],{"class":697}," SelectableProps",[681,1969,1970],{"class":701},") {\n",[681,1972,1973,1975],{"class":545,"line":690},[681,1974,731],{"class":693},[681,1976,734],{"class":701},[681,1978,1979,1981,1983],{"class":545,"line":728},[681,1980,740],{"class":701},[681,1982,901],{"class":743},[681,1984,747],{"class":701},[681,1986,1987,1990,1992,1994,1996,1998,2000],{"class":545,"line":737},[681,1988,1989],{"class":701},"      {rows.",[681,1991,912],{"class":697},[681,1993,915],{"class":701},[681,1995,918],{"class":705},[681,1997,921],{"class":701},[681,1999,924],{"class":693},[681,2001,734],{"class":701},[681,2003,2004,2006,2008,2010,2012,2015,2018,2020,2023,2026],{"class":545,"line":750},[681,2005,791],{"class":701},[681,2007,794],{"class":743},[681,2009,936],{"class":697},[681,2011,822],{"class":693},[681,2013,2014],{"class":701},"{row.id} ",[681,2016,2017],{"class":697},"aria-selected",[681,2019,822],{"class":693},[681,2021,2022],{"class":701},"{selected.",[681,2024,2025],{"class":697},"has",[681,2027,2028],{"class":701},"(row.id)}>\n",[681,2030,2031,2033,2035],{"class":545,"line":762},[681,2032,813],{"class":701},[681,2034,982],{"class":743},[681,2036,747],{"class":701},[681,2038,2039,2041],{"class":545,"line":778},[681,2040,958],{"class":701},[681,2042,2043],{"class":743},"input\n",[681,2045,2046,2049,2051],{"class":545,"line":788},[681,2047,2048],{"class":697},"              type",[681,2050,822],{"class":693},[681,2052,2053],{"class":825},"\"checkbox\"\n",[681,2055,2056,2059,2061,2063,2065],{"class":545,"line":799},[681,2057,2058],{"class":697},"              checked",[681,2060,822],{"class":693},[681,2062,2022],{"class":701},[681,2064,2025],{"class":697},[681,2066,2067],{"class":701},"(row.id)}\n",[681,2069,2070,2073,2075,2077,2079,2082],{"class":545,"line":810},[681,2071,2072],{"class":697},"              onChange",[681,2074,822],{"class":693},[681,2076,1794],{"class":701},[681,2078,924],{"class":693},[681,2080,2081],{"class":697}," toggle",[681,2083,2067],{"class":701},[681,2085,2086],{"class":545,"line":836},[681,2087,2088],{"class":686},"              \u002F\u002F Name the checkbox by the row it controls — never a bare \"select\"\n",[681,2090,2091,2094,2096,2099,2102,2104,2106,2109,2112],{"class":545,"line":856},[681,2092,2093],{"class":697},"              aria-label",[681,2095,822],{"class":693},[681,2097,2098],{"class":701},"{",[681,2100,2101],{"class":825},"`Select ${",[681,2103,918],{"class":701},[681,2105,362],{"class":825},[681,2107,2108],{"class":701},"region",[681,2110,2111],{"class":825},"}`",[681,2113,759],{"class":701},[681,2115,2116],{"class":545,"line":876},[681,2117,2118],{"class":701},"            \u002F>\n",[681,2120,2121,2123,2125],{"class":545,"line":886},[681,2122,1009],{"class":701},[681,2124,982],{"class":743},[681,2126,747],{"class":701},[681,2128,2129,2131,2133,2135,2137,2139,2141,2143],{"class":545,"line":896},[681,2130,813],{"class":701},[681,2132,816],{"class":743},[681,2134,819],{"class":697},[681,2136,822],{"class":693},[681,2138,967],{"class":825},[681,2140,970],{"class":701},[681,2142,816],{"class":743},[681,2144,747],{"class":701},[681,2146,2147,2149,2151,2153,2155],{"class":545,"line":906},[681,2148,813],{"class":701},[681,2150,982],{"class":743},[681,2152,985],{"class":701},[681,2154,982],{"class":743},[681,2156,747],{"class":701},[681,2158,2159,2161,2163],{"class":545,"line":929},[681,2160,879],{"class":701},[681,2162,794],{"class":743},[681,2164,747],{"class":701},[681,2166,2167],{"class":545,"line":944},[681,2168,2169],{"class":701},"      ))}\n",[681,2171,2172,2174,2176],{"class":545,"line":955},[681,2173,1034],{"class":701},[681,2175,901],{"class":743},[681,2177,747],{"class":701},[681,2179,2180],{"class":545,"line":977},[681,2181,1044],{"class":701},[681,2183,2184],{"class":545,"line":992},[681,2185,759],{"class":701},[324,2187,2188,2189,2192,2193,362],{},"Two notes. First, set the \"select all\" checkbox's ",[328,2190,2191],{},"indeterminate"," DOM property via a ref when the selection is partial—it is not an HTML attribute. Second, announce the running selection count (\"3 rows selected\") through a polite live region so the state change is perceivable without sight, exactly as covered in ",[336,2194,133],{"href":342},[1097,2196,2197],{},[324,2198,2199,2201],{},[366,2200,1103],{}," Navigate with the keyboard alone and confirm every checkbox is reachable and clearly named. Verify the select-all checkbox announces \"partially checked\" \u002F \"mixed\" when some but not all rows are selected.",[424,2203],{},[427,2205,2207,2208,2211,2212,2215],{"id":2206},"when-to-use-rolegrid-treegrid-and-when-not-to","When to Use ",[328,2209,2210],{},"role=\"grid\""," \u002F ",[328,2213,2214],{},"treegrid"," — and When Not To",[324,2217,2218,2219,2221,2222,2225,2226,2229,2230,2233],{},"This is the most over-reached-for pattern in table accessibility. ",[328,2220,2210],{}," is ",[366,2223,2224],{},"not"," a styling hint or a generic \"data table\" role. It is an interaction model: a grid is a widget where arrow keys move a single focus point between cells, like a spreadsheet, and the entire grid is a single tab stop. Adopting it commits you to implementing full roving-tabindex keyboard navigation, focus management, and ",[328,2227,2228],{},"aria-activedescendant"," or per-cell ",[328,2231,2232],{},"tabindex"," orchestration.",[324,2235,2236,2237,2239,2240,2242,2243,2245],{},"Most \"data tables\"—even ones with sorting, selection, and pagination—are ",[366,2238,2224],{}," grids. They are static tabular content with a few interactive controls (a sort button, a checkbox) that are individually in the tab order. For these, a plain semantic ",[328,2241,628],{}," is correct, more robust, and far less code. Reaching for ",[328,2244,2210],{}," here usually makes accessibility worse, because half-implemented grid keyboard support is more confusing than no grid behavior at all.",[324,2247,2248,2249,2251,2252,2255,2256,2259,2260,2263,2264,2267],{},"Use ",[328,2250,2210],{}," only when the cells themselves are the interactive surface and users expect spreadsheet-style arrow navigation: editable data grids, calendar date pickers, and the like. Use ",[328,2253,2254],{},"role=\"treegrid\""," only when rows are additionally expandable\u002Fcollapsible in a hierarchy. If you adopt a grid, the ",[336,2257,2258],{"href":360},"virtualization guide"," covers the ",[328,2261,2262],{},"aria-rowcount"," and ",[328,2265,2266],{},"aria-rowindex"," attributes that grids require once the DOM holds only a slice of rows.",[1097,2269,2270],{},[324,2271,2272,2274,2275,2277],{},[366,2273,1103],{}," Ask yourself: \"Do users navigate this with arrow keys between cells?\" If no, do not use ",[328,2276,2210],{},". If yes, verify a single tab stop enters the grid and arrow keys move focus across cells.",[424,2279],{},[427,2281,2283],{"id":2282},"pagination-and-announcing-result-counts","Pagination and Announcing Result Counts",[324,2285,2286,2287,2290,2291,2294],{},"Paginated tables present two distinct accessibility problems. The first is the pagination control itself: it should be a ",[328,2288,2289],{},"\u003Cnav aria-label=\"Pagination\">"," whose current page carries ",[328,2292,2293],{},"aria-current=\"page\"",", with prev\u002Fnext controls that expose a disabled state at the boundaries. The second is announcing what changed after a page click—the visible row range and total—so a screen reader user knows the page turned.",[672,2296,2298],{"className":674,"code":2297,"language":676,"meta":677,"style":677},"\u003Cnav aria-label=\"Pagination\">\n  \u003Cul>\n    \u003Cli>\n      \u003Cbutton type=\"button\" disabled={page === 1} onClick={prev}>\n        Previous\n      \u003C\u002Fbutton>\n    \u003C\u002Fli>\n    {pages.map((p) => (\n      \u003Cli key={p}>\n        \u003Cbutton\n          type=\"button\"\n          \u002F\u002F aria-current marks the active page for AT and styling\n          aria-current={p === page ? 'page' : undefined}\n          onClick={() => goTo(p)}\n        >\n          {p}\n        \u003C\u002Fbutton>\n      \u003C\u002Fli>\n    ))}\n    \u003Cli>\n      \u003Cbutton type=\"button\" disabled={page === lastPage} onClick={next}>\n        Next\n      \u003C\u002Fbutton>\n    \u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Fnav>\n\n{\u002F* Polite status: announces the new slice after each page change *\u002F}\n\u003Cp role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n  Showing {start}–{end} of {total} results\n\u003C\u002Fp>\n",[328,2299,2300,2317,2325,2333,2369,2374,2382,2390,2407,2420,2427,2437,2442,2469,2486,2491,2496,2504,2512,2517,2525,2555,2560,2568,2576,2584,2592,2596,2605,2638,2644],{"__ignoreMap":677},[681,2301,2302,2304,2307,2310,2312,2315],{"class":545,"line":683},[681,2303,1179],{"class":701},[681,2305,2306],{"class":743},"nav",[681,2308,2309],{"class":697}," aria-label",[681,2311,822],{"class":693},[681,2313,2314],{"class":825},"\"Pagination\"",[681,2316,747],{"class":701},[681,2318,2319,2321,2323],{"class":545,"line":690},[681,2320,1188],{"class":701},[681,2322,370],{"class":743},[681,2324,747],{"class":701},[681,2326,2327,2329,2331],{"class":545,"line":728},[681,2328,740],{"class":701},[681,2330,373],{"class":743},[681,2332,747],{"class":701},[681,2334,2335,2337,2339,2341,2343,2345,2348,2350,2353,2355,2358,2361,2364,2366],{"class":545,"line":737},[681,2336,765],{"class":701},[681,2338,1778],{"class":743},[681,2340,1781],{"class":697},[681,2342,822],{"class":693},[681,2344,1786],{"class":825},[681,2346,2347],{"class":697}," disabled",[681,2349,822],{"class":693},[681,2351,2352],{"class":701},"{page ",[681,2354,1709],{"class":693},[681,2356,2357],{"class":1620}," 1",[681,2359,2360],{"class":701},"} ",[681,2362,2363],{"class":697},"onClick",[681,2365,822],{"class":693},[681,2367,2368],{"class":701},"{prev}>\n",[681,2370,2371],{"class":545,"line":750},[681,2372,2373],{"class":701},"        Previous\n",[681,2375,2376,2378,2380],{"class":545,"line":762},[681,2377,889],{"class":701},[681,2379,1778],{"class":743},[681,2381,747],{"class":701},[681,2383,2384,2386,2388],{"class":545,"line":778},[681,2385,1034],{"class":701},[681,2387,373],{"class":743},[681,2389,747],{"class":701},[681,2391,2392,2395,2397,2399,2401,2403,2405],{"class":545,"line":788},[681,2393,2394],{"class":701},"    {pages.",[681,2396,912],{"class":697},[681,2398,915],{"class":701},[681,2400,324],{"class":705},[681,2402,921],{"class":701},[681,2404,924],{"class":693},[681,2406,734],{"class":701},[681,2408,2409,2411,2413,2415,2417],{"class":545,"line":799},[681,2410,765],{"class":701},[681,2412,373],{"class":743},[681,2414,936],{"class":697},[681,2416,822],{"class":693},[681,2418,2419],{"class":701},"{p}>\n",[681,2421,2422,2424],{"class":545,"line":810},[681,2423,791],{"class":701},[681,2425,2426],{"class":743},"button\n",[681,2428,2429,2432,2434],{"class":545,"line":836},[681,2430,2431],{"class":697},"          type",[681,2433,822],{"class":693},[681,2435,2436],{"class":825},"\"button\"\n",[681,2438,2439],{"class":545,"line":856},[681,2440,2441],{"class":686},"          \u002F\u002F aria-current marks the active page for AT and styling\n",[681,2443,2444,2447,2449,2452,2454,2457,2459,2462,2464,2467],{"class":545,"line":876},[681,2445,2446],{"class":697},"          aria-current",[681,2448,822],{"class":693},[681,2450,2451],{"class":701},"{p ",[681,2453,1709],{"class":693},[681,2455,2456],{"class":701}," page ",[681,2458,1736],{"class":693},[681,2460,2461],{"class":825}," 'page'",[681,2463,1856],{"class":693},[681,2465,2466],{"class":1620}," undefined",[681,2468,759],{"class":701},[681,2470,2471,2474,2476,2478,2480,2483],{"class":545,"line":886},[681,2472,2473],{"class":697},"          onClick",[681,2475,822],{"class":693},[681,2477,1794],{"class":701},[681,2479,924],{"class":693},[681,2481,2482],{"class":697}," goTo",[681,2484,2485],{"class":701},"(p)}\n",[681,2487,2488],{"class":545,"line":896},[681,2489,2490],{"class":701},"        >\n",[681,2492,2493],{"class":545,"line":906},[681,2494,2495],{"class":701},"          {p}\n",[681,2497,2498,2500,2502],{"class":545,"line":929},[681,2499,879],{"class":701},[681,2501,1778],{"class":743},[681,2503,747],{"class":701},[681,2505,2506,2508,2510],{"class":545,"line":944},[681,2507,889],{"class":701},[681,2509,373],{"class":743},[681,2511,747],{"class":701},[681,2513,2514],{"class":545,"line":955},[681,2515,2516],{"class":701},"    ))}\n",[681,2518,2519,2521,2523],{"class":545,"line":977},[681,2520,740],{"class":701},[681,2522,373],{"class":743},[681,2524,747],{"class":701},[681,2526,2527,2529,2531,2533,2535,2537,2539,2541,2543,2545,2548,2550,2552],{"class":545,"line":992},[681,2528,765],{"class":701},[681,2530,1778],{"class":743},[681,2532,1781],{"class":697},[681,2534,822],{"class":693},[681,2536,1786],{"class":825},[681,2538,2347],{"class":697},[681,2540,822],{"class":693},[681,2542,2352],{"class":701},[681,2544,1709],{"class":693},[681,2546,2547],{"class":701}," lastPage} ",[681,2549,2363],{"class":697},[681,2551,822],{"class":693},[681,2553,2554],{"class":701},"{next}>\n",[681,2556,2557],{"class":545,"line":1006},[681,2558,2559],{"class":701},"        Next\n",[681,2561,2562,2564,2566],{"class":545,"line":1016},[681,2563,889],{"class":701},[681,2565,1778],{"class":743},[681,2567,747],{"class":701},[681,2569,2570,2572,2574],{"class":545,"line":1022},[681,2571,1034],{"class":701},[681,2573,373],{"class":743},[681,2575,747],{"class":701},[681,2577,2578,2580,2582],{"class":545,"line":1031},[681,2579,1305],{"class":701},[681,2581,370],{"class":743},[681,2583,747],{"class":701},[681,2585,2586,2588,2590],{"class":545,"line":1041},[681,2587,1422],{"class":701},[681,2589,2306],{"class":743},[681,2591,747],{"class":701},[681,2593,2594],{"class":545,"line":1047},[681,2595,1537],{"emptyLinePlaceholder":1536},[681,2597,2598,2600,2603],{"class":545,"line":1899},[681,2599,2098],{"class":701},[681,2601,2602],{"class":686},"\u002F* Polite status: announces the new slice after each page change *\u002F",[681,2604,759],{"class":701},[681,2606,2608,2610,2612,2615,2617,2620,2623,2625,2628,2631,2633,2636],{"class":545,"line":2607},29,[681,2609,1179],{"class":701},[681,2611,324],{"class":743},[681,2613,2614],{"class":697}," role",[681,2616,822],{"class":693},[681,2618,2619],{"class":825},"\"status\"",[681,2621,2622],{"class":697}," aria-live",[681,2624,822],{"class":693},[681,2626,2627],{"class":825},"\"polite\"",[681,2629,2630],{"class":697}," className",[681,2632,822],{"class":693},[681,2634,2635],{"class":825},"\"sr-only\"",[681,2637,747],{"class":701},[681,2639,2641],{"class":545,"line":2640},30,[681,2642,2643],{"class":701},"  Showing {start}–{end} of {total} results\n",[681,2645,2647,2649,2651],{"class":545,"line":2646},31,[681,2648,1422],{"class":701},[681,2650,324],{"class":743},[681,2652,747],{"class":701},[324,2654,2655,2656,2658,2659,2662],{},"The \"showing X–Y of N\" message is a textbook ",[328,2657,393],{}," use case: it conveys a state change without moving focus or interrupting the user. The ",[336,2660,2661],{"href":355},"pagination deep dive"," covers focus handling after a page change—where to send focus so keyboard users land on the refreshed data rather than the bottom of the page.",[1097,2664,2665],{},[324,2666,2667,2669,2670,2672],{},[366,2668,1103],{}," Click a page, then confirm the screen reader announces the new range without you moving focus. Tab through the nav and verify ",[328,2671,2293],{}," lands on exactly the active page and disabled prev\u002Fnext are skipped or announced as unavailable.",[424,2674],{},[427,2676,2678],{"id":2677},"common-a11y-mistakes","Common a11y Mistakes",[370,2680,2681,2690,2700,2708,2720,2729,2738],{},[373,2682,2683,2689],{},[366,2684,2685,2686,2688],{},"Rebuilding tables from ",[328,2687,330],{},"s",", destroying every header-to-cell relationship and breaking screen reader table navigation.",[373,2691,2692,2699],{},[366,2693,2694,2695,1059,2697],{},"Breaking native table roles with ",[328,2696,1058],{},[328,2698,1062],{}," on table elements without restoring roles or re-testing.",[373,2701,2702,2707],{},[366,2703,2704,2705],{},"Omitting ",[328,2706,410],{}," or substituting an unassociated heading as the table's name.",[373,2709,2710,2719],{},[366,2711,2712,2713,2715,2716,2718],{},"Making sort headers clickable ",[328,2714,666],{},"s instead of ",[328,2717,1473],{},"s inside them",", leaving the control unreachable by keyboard.",[373,2721,2722,2728],{},[366,2723,2724,2725,2727],{},"Setting ",[328,2726,1461],{}," on every sortable header at once"," (or never updating it), so AT cannot tell which column is sorted.",[373,2730,2731,2737],{},[366,2732,2733,2734,2736],{},"Reaching for ",[328,2735,2210],{}," on static tables",", committing to spreadsheet keyboard navigation that is then only half-implemented.",[373,2739,2740,2743],{},[366,2741,2742],{},"Reordering rows or turning pages silently",", leaving screen reader users unaware the data changed because no live region announced it.",[424,2745],{},[427,2747,2749],{"id":2748},"frequently-asked-questions","Frequently Asked Questions",[324,2751,2752,2761,2762,2764,2765,2767],{},[366,2753,2754,2755,2757,2758,2760],{},"When should I use a native ",[328,2756,628],{}," versus ",[328,2759,2210],{}," in React?","\nUse a native ",[328,2763,628],{}," for virtually all data display, including tables with sorting, selection, and pagination, because those controls are individually focusable and need no special keyboard model. Use ",[328,2766,2210],{}," only for spreadsheet-like widgets where users navigate between cells with arrow keys and the whole grid is a single tab stop, since that role commits you to implementing roving focus and cell-level keyboard navigation.",[324,2769,2770,2773,2774,2776,2777,1059,2779,2781,2782,352,2784,1068,2786,2788,2789,352,2791,357,2793,2795],{},[366,2771,2772],{},"Why does my table lose its accessibility when I add CSS Grid or Flexbox?","\nThe accessibility role of an element is derived from its computed ",[328,2775,1074],{}," value. Applying ",[328,2778,1058],{},[328,2780,1062],{}," to a ",[328,2783,628],{},[328,2785,663],{},[328,2787,669],{}," can override the implicit table, row, or cell role in some browsers, leaving assistive technology with no table structure to navigate. Either keep table semantics and avoid those overrides, or restore the roles explicitly with ",[328,2790,1085],{},[328,2792,1088],{},[328,2794,1094],{}," and re-test in a screen reader.",[324,2797,2798,2801,2802,1059,2805,2808,2809,2811,2812,2815,2816,2818],{},[366,2799,2800],{},"How do screen reader users know a table column was sorted?","\nTwo mechanisms work together. Set ",[328,2803,2804],{},"aria-sort=\"ascending\"",[328,2806,2807],{},"\"descending\""," on the active column's ",[328,2810,666],{}," so the direction is exposed as a property of the header, and announce the change through a polite ",[328,2813,2814],{},"aria-live"," region with a message like \"Table sorted by Revenue, descending.\" The ",[328,2817,1461],{}," value alone is read when the user revisits the header, while the live region informs users who keep their place in the data after sorting.",[324,2820,2821,2828,2829,2831,2832,2835,2836,362],{},[366,2822,2823,2824,2827],{},"Do I need a live region for pagination, or is ",[328,2825,2826],{},"aria-current"," enough?","\nYou need both. ",[328,2830,2293],{}," identifies the active page button within the pagination ",[328,2833,2834],{},"\u003Cnav>",", which helps when a user tabs through the control. But it does not tell users that the visible rows changed when they click a page. A polite live region announcing \"Showing 21–40 of 312 results\" communicates the actual data change, satisfying ",[328,2837,393],{},[324,2839,2840,2843,2844,2846,2847,2849],{},[366,2841,2842],{},"Is it accessible to virtualize a long table with react-window?","\nOnly if you preserve the table's true size in the accessibility tree. Virtualization keeps just a slice of rows in the DOM, so assistive technology would otherwise report a tiny table. Expose the real dimensions with ",[328,2845,2262],{}," on the grid and ",[328,2848,2266],{}," on each rendered row, and manage focus carefully as rows unmount. If the dataset is small enough to render fully without performance problems, skip virtualization entirely—a plain table is more robust.",[424,2851],{},[427,2853,2855],{"id":2854},"related-guides","Related guides",[370,2857,2858,2862,2866,2870,2874,2878],{},[373,2859,2860],{},[336,2861,94],{"href":338},[373,2863,2864],{},[336,2865,97],{"href":346},[373,2867,2868],{},[336,2869,133],{"href":342},[373,2871,2872],{},[336,2873,121],{"href":350},[373,2875,2876],{},[336,2877,115],{"href":355},[373,2879,2880],{},[336,2881,127],{"href":360},[2883,2884,2885],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":677,"searchDepth":690,"depth":690,"links":2887},[2888,2889,2891,2893,2895,2896,2898,2899,2900,2901],{"id":429,"depth":690,"text":430},{"id":624,"depth":690,"text":2890},"Semantic \u003Ctable> vs \u003Cdiv> Grids",{"id":1123,"depth":690,"text":2892},"Caption, scope, and Header Associations",{"id":1457,"depth":690,"text":2894},"Sortable Columns With aria-sort",{"id":1926,"depth":690,"text":1927},{"id":2206,"depth":690,"text":2897},"When to Use role=\"grid\" \u002F treegrid — and When Not To",{"id":2282,"depth":690,"text":2283},{"id":2677,"depth":690,"text":2678},{"id":2748,"depth":690,"text":2749},{"id":2854,"depth":690,"text":2855},null,"Build screen-reader-friendly data tables in React—semantic table markup, sortable headers with aria-sort, accessible pagination, row selection, and virtualized lists.","md",{},false,{"title":109,"description":2903},"rWkSf8EpYqCe9M5EkPd5MmETWTjKopP_KjFpCjOavxU",[2910,2949,2950,3013],{"title":5,"path":6,"stem":7,"children":2911},[2912,2913,2916,2919,2925,2931,2940,2946],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2914},[2915],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2917},[2918],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2920},[2921,2922],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2923},[2924],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2926},[2927,2928],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2929},[2930],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2932},[2933,2934,2937],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2935},[2936],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2938},[2939],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2941},[2942,2943],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2944},[2945],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2947},[2948],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2951},[2952,2953,2959,2971,2983,2986,2995,3007],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2954},[2955,2956],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2957},[2958],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2960},[2961,2962,2965,2968],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2963},[2964],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2966},[2967],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2969},[2970],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2972},[2973,2974,2977,2980],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2975},[2976],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2978},[2979],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2981},[2982],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2984},[2985],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2987},[2988,2989,2992],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2990},[2991],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2993},[2994],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2996},[2997,2998,3001,3004],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2999},[3000],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":3002},[3003],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":3005},[3006],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":3008},[3009,3010],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":3011},[3012],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":3014},[3015,3016,3025,3034,3043,3052],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":3017},[3018,3019,3022],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":3020},[3021],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":3023},[3024],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":3026},[3027,3028,3031],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":3029},[3030],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":3032},[3033],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":3035},[3036,3037,3040],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":3038},[3039],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":3041},[3042],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":3044},[3045,3046,3049],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":3047},[3048],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":3050},[3051],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":3053},[3054,3055,3058],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":3056},[3057],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":3059},[3060],{"title":309,"path":310,"stem":311},1781785523591]