[{"data":1,"prerenderedAt":2242},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002F":314,"content-navigation":2090},[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":115,"body":316,"date":2083,"description":2084,"extension":2085,"image":2083,"meta":2086,"modifiedAt":2083,"navigation":452,"noindex":2087,"path":116,"publishedAt":2083,"seo":2088,"stem":117,"updatedAt":2083,"__hash__":2089},"content\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002Findex.md",{"type":317,"value":318,"toc":2070},"minimark",[319,323,340,348,354,380,383,391,407,1137,1148,1150,1156,1166,1177,1251,1262,1264,1268,1279,1289,1291,1295,1301,1720,1727,1729,1733,1738,1762,1850,1860,1862,1866,1901,1916,1918,1922,1980,1982,1986,2007,2018,2032,2044,2046,2050,2066],[320,321,115],"h1",{"id":322},"accessible-pagination-for-react-data-tables",[324,325,326,327,331,332,335,336,339],"p",{},"Pagination looks trivial and breaks accessibility in three predictable ways: the control is built from ",[328,329,330],"code",{},"\u003Cdiv>","s or bare links with no landmark, the active page is shown only with color, and the table silently swaps its rows so screen reader users never learn the page turned. This guide builds pagination that solves all three: a labeled ",[328,333,334],{},"\u003Cnav>",", ",[328,337,338],{},"aria-current=\"page\""," on the active control, and a polite live region announcing the new visible range after every change.",[324,341,342,343,347],{},"This is a deep dive under ",[344,345,109],"a",{"href":346},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002F",". It assumes you already have an accessible table and focuses entirely on the pagination control and the state changes it triggers.",[324,349,350],{},[351,352,353],"strong",{},"Mapped WCAG Success Criteria:",[355,356,357,364,369,375],"ul",{},[358,359,360,363],"li",{},[328,361,362],{},"4.1.2 Name, Role, Value"," (Level A)",[358,365,366,363],{},[328,367,368],{},"2.1.1 Keyboard",[358,370,371,374],{},[328,372,373],{},"4.1.3 Status Messages"," (Level AA)",[358,376,377,363],{},[328,378,379],{},"2.4.3 Focus Order",[381,382],"hr",{},[384,385,387,388,390],"h2",{"id":386},"a-labeled-nav-for-the-pagination-control","A Labeled ",[328,389,334],{}," for the Pagination Control",[324,392,393,394,396,397,399,400,403,404,406],{},"Pagination is navigation, so it belongs in a ",[328,395,334],{}," landmark. Because most pages contain more than one ",[328,398,334],{}," (primary navigation, breadcrumbs, pagination), give it an ",[328,401,402],{},"aria-label"," so assistive technology can distinguish them—",[328,405,362],{}," requires the landmark to have a discernible name. Inside, an ordered or unordered list communicates that the page links form a set.",[408,409,414],"pre",{"className":410,"code":411,"language":412,"meta":413,"style":413},"language-tsx shiki shiki-themes github-light github-dark","'use client';\nimport { useRef } from 'react';\n\ntype PaginationProps = {\n  page: number;\n  pageCount: number;\n  onChange: (page: number) => void;\n};\n\nexport function Pagination({ page, pageCount, onChange }: PaginationProps) {\n  const pages = Array.from({ length: pageCount }, (_, i) => i + 1);\n\n  return (\n    \u002F\u002F aria-label distinguishes this nav from other landmarks on the page\n    \u003Cnav aria-label=\"Pagination\">\n      \u003Cul className=\"pagination-list\">\n        \u003Cli>\n          {\u002F* disabled at the lower boundary — exposed as unavailable, not hidden *\u002F}\n          \u003Cbutton type=\"button\" disabled={page === 1} onClick={() => onChange(page - 1)}>\n            \u003Cspan aria-hidden=\"true\">‹\u003C\u002Fspan>\n            \u003Cspan className=\"sr-only\">Previous page\u003C\u002Fspan>\n          \u003C\u002Fbutton>\n        \u003C\u002Fli>\n\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 for styling\n              aria-current={p === page ? 'page' : undefined}\n              aria-label={`Page ${p}`}\n              onClick={() => onChange(p)}\n            >\n              {p}\n            \u003C\u002Fbutton>\n          \u003C\u002Fli>\n        ))}\n\n        \u003Cli>\n          \u003Cbutton type=\"button\" disabled={page === pageCount} onClick={() => onChange(page + 1)}>\n            \u003Cspan aria-hidden=\"true\">›\u003C\u002Fspan>\n            \u003Cspan className=\"sr-only\">Next page\u003C\u002Fspan>\n          \u003C\u002Fbutton>\n        \u003C\u002Fli>\n      \u003C\u002Ful>\n    \u003C\u002Fnav>\n  );\n}\n","tsx","",[328,415,416,429,447,454,470,486,498,527,533,538,575,618,623,632,639,661,679,689,701,758,781,802,812,822,827,847,862,870,881,887,917,938,955,961,967,977,986,992,997,1006,1048,1068,1088,1097,1106,1116,1126,1132],{"__ignoreMap":413},[417,418,421,425],"span",{"class":419,"line":420},"line",1,[417,422,424],{"class":423},"sZZnC","'use client'",[417,426,428],{"class":427},"sVt8B",";\n",[417,430,432,436,439,442,445],{"class":419,"line":431},2,[417,433,435],{"class":434},"szBVR","import",[417,437,438],{"class":427}," { useRef } ",[417,440,441],{"class":434},"from",[417,443,444],{"class":423}," 'react'",[417,446,428],{"class":427},[417,448,450],{"class":419,"line":449},3,[417,451,453],{"emptyLinePlaceholder":452},true,"\n",[417,455,457,460,464,467],{"class":419,"line":456},4,[417,458,459],{"class":434},"type",[417,461,463],{"class":462},"sScJk"," PaginationProps",[417,465,466],{"class":434}," =",[417,468,469],{"class":427}," {\n",[417,471,473,477,480,484],{"class":419,"line":472},5,[417,474,476],{"class":475},"s4XuR","  page",[417,478,479],{"class":434},":",[417,481,483],{"class":482},"sj4cs"," number",[417,485,428],{"class":427},[417,487,489,492,494,496],{"class":419,"line":488},6,[417,490,491],{"class":475},"  pageCount",[417,493,479],{"class":434},[417,495,483],{"class":482},[417,497,428],{"class":427},[417,499,501,504,506,509,512,514,516,519,522,525],{"class":419,"line":500},7,[417,502,503],{"class":462},"  onChange",[417,505,479],{"class":434},[417,507,508],{"class":427}," (",[417,510,511],{"class":475},"page",[417,513,479],{"class":434},[417,515,483],{"class":482},[417,517,518],{"class":427},") ",[417,520,521],{"class":434},"=>",[417,523,524],{"class":482}," void",[417,526,428],{"class":427},[417,528,530],{"class":419,"line":529},8,[417,531,532],{"class":427},"};\n",[417,534,536],{"class":419,"line":535},9,[417,537,453],{"emptyLinePlaceholder":452},[417,539,541,544,547,550,553,555,557,560,562,565,568,570,572],{"class":419,"line":540},10,[417,542,543],{"class":434},"export",[417,545,546],{"class":434}," function",[417,548,549],{"class":462}," Pagination",[417,551,552],{"class":427},"({ ",[417,554,511],{"class":475},[417,556,335],{"class":427},[417,558,559],{"class":475},"pageCount",[417,561,335],{"class":427},[417,563,564],{"class":475},"onChange",[417,566,567],{"class":427}," }",[417,569,479],{"class":434},[417,571,463],{"class":462},[417,573,574],{"class":427},") {\n",[417,576,578,581,584,586,589,591,594,597,599,602,604,606,609,612,615],{"class":419,"line":577},11,[417,579,580],{"class":434},"  const",[417,582,583],{"class":482}," pages",[417,585,466],{"class":434},[417,587,588],{"class":427}," Array.",[417,590,441],{"class":462},[417,592,593],{"class":427},"({ length: pageCount }, (",[417,595,596],{"class":475},"_",[417,598,335],{"class":427},[417,600,601],{"class":475},"i",[417,603,518],{"class":427},[417,605,521],{"class":434},[417,607,608],{"class":427}," i ",[417,610,611],{"class":434},"+",[417,613,614],{"class":482}," 1",[417,616,617],{"class":427},");\n",[417,619,621],{"class":419,"line":620},12,[417,622,453],{"emptyLinePlaceholder":452},[417,624,626,629],{"class":419,"line":625},13,[417,627,628],{"class":434},"  return",[417,630,631],{"class":427}," (\n",[417,633,635],{"class":419,"line":634},14,[417,636,638],{"class":637},"sJ8bj","    \u002F\u002F aria-label distinguishes this nav from other landmarks on the page\n",[417,640,642,645,649,652,655,658],{"class":419,"line":641},15,[417,643,644],{"class":427},"    \u003C",[417,646,648],{"class":647},"s9eBZ","nav",[417,650,651],{"class":462}," aria-label",[417,653,654],{"class":434},"=",[417,656,657],{"class":423},"\"Pagination\"",[417,659,660],{"class":427},">\n",[417,662,664,667,669,672,674,677],{"class":419,"line":663},16,[417,665,666],{"class":427},"      \u003C",[417,668,355],{"class":647},[417,670,671],{"class":462}," className",[417,673,654],{"class":434},[417,675,676],{"class":423},"\"pagination-list\"",[417,678,660],{"class":427},[417,680,682,685,687],{"class":419,"line":681},17,[417,683,684],{"class":427},"        \u003C",[417,686,358],{"class":647},[417,688,660],{"class":427},[417,690,692,695,698],{"class":419,"line":691},18,[417,693,694],{"class":427},"          {",[417,696,697],{"class":637},"\u002F* disabled at the lower boundary — exposed as unavailable, not hidden *\u002F",[417,699,700],{"class":427},"}\n",[417,702,704,707,710,713,715,718,721,723,726,729,731,734,737,739,742,744,747,750,753,755],{"class":419,"line":703},19,[417,705,706],{"class":427},"          \u003C",[417,708,709],{"class":647},"button",[417,711,712],{"class":462}," type",[417,714,654],{"class":434},[417,716,717],{"class":423},"\"button\"",[417,719,720],{"class":462}," disabled",[417,722,654],{"class":434},[417,724,725],{"class":427},"{page ",[417,727,728],{"class":434},"===",[417,730,614],{"class":482},[417,732,733],{"class":427},"} ",[417,735,736],{"class":462},"onClick",[417,738,654],{"class":434},[417,740,741],{"class":427},"{() ",[417,743,521],{"class":434},[417,745,746],{"class":462}," onChange",[417,748,749],{"class":427},"(page ",[417,751,752],{"class":434},"-",[417,754,614],{"class":482},[417,756,757],{"class":427},")}>\n",[417,759,761,764,766,769,771,774,777,779],{"class":419,"line":760},20,[417,762,763],{"class":427},"            \u003C",[417,765,417],{"class":647},[417,767,768],{"class":462}," aria-hidden",[417,770,654],{"class":434},[417,772,773],{"class":423},"\"true\"",[417,775,776],{"class":427},">‹\u003C\u002F",[417,778,417],{"class":647},[417,780,660],{"class":427},[417,782,784,786,788,790,792,795,798,800],{"class":419,"line":783},21,[417,785,763],{"class":427},[417,787,417],{"class":647},[417,789,671],{"class":462},[417,791,654],{"class":434},[417,793,794],{"class":423},"\"sr-only\"",[417,796,797],{"class":427},">Previous page\u003C\u002F",[417,799,417],{"class":647},[417,801,660],{"class":427},[417,803,805,808,810],{"class":419,"line":804},22,[417,806,807],{"class":427},"          \u003C\u002F",[417,809,709],{"class":647},[417,811,660],{"class":427},[417,813,815,818,820],{"class":419,"line":814},23,[417,816,817],{"class":427},"        \u003C\u002F",[417,819,358],{"class":647},[417,821,660],{"class":427},[417,823,825],{"class":419,"line":824},24,[417,826,453],{"emptyLinePlaceholder":452},[417,828,830,833,836,839,841,843,845],{"class":419,"line":829},25,[417,831,832],{"class":427},"        {pages.",[417,834,835],{"class":462},"map",[417,837,838],{"class":427},"((",[417,840,324],{"class":475},[417,842,518],{"class":427},[417,844,521],{"class":434},[417,846,631],{"class":427},[417,848,850,852,854,857,859],{"class":419,"line":849},26,[417,851,706],{"class":427},[417,853,358],{"class":647},[417,855,856],{"class":462}," key",[417,858,654],{"class":434},[417,860,861],{"class":427},"{p}>\n",[417,863,865,867],{"class":419,"line":864},27,[417,866,763],{"class":427},[417,868,869],{"class":647},"button\n",[417,871,873,876,878],{"class":419,"line":872},28,[417,874,875],{"class":462},"              type",[417,877,654],{"class":434},[417,879,880],{"class":423},"\"button\"\n",[417,882,884],{"class":419,"line":883},29,[417,885,886],{"class":637},"              \u002F\u002F aria-current marks the active page for AT and for styling\n",[417,888,890,893,895,898,900,903,906,909,912,915],{"class":419,"line":889},30,[417,891,892],{"class":462},"              aria-current",[417,894,654],{"class":434},[417,896,897],{"class":427},"{p ",[417,899,728],{"class":434},[417,901,902],{"class":427}," page ",[417,904,905],{"class":434},"?",[417,907,908],{"class":423}," 'page'",[417,910,911],{"class":434}," :",[417,913,914],{"class":482}," undefined",[417,916,700],{"class":427},[417,918,920,923,925,928,931,933,936],{"class":419,"line":919},31,[417,921,922],{"class":462},"              aria-label",[417,924,654],{"class":434},[417,926,927],{"class":427},"{",[417,929,930],{"class":423},"`Page ${",[417,932,324],{"class":427},[417,934,935],{"class":423},"}`",[417,937,700],{"class":427},[417,939,941,944,946,948,950,952],{"class":419,"line":940},32,[417,942,943],{"class":462},"              onClick",[417,945,654],{"class":434},[417,947,741],{"class":427},[417,949,521],{"class":434},[417,951,746],{"class":462},[417,953,954],{"class":427},"(p)}\n",[417,956,958],{"class":419,"line":957},33,[417,959,960],{"class":427},"            >\n",[417,962,964],{"class":419,"line":963},34,[417,965,966],{"class":427},"              {p}\n",[417,968,970,973,975],{"class":419,"line":969},35,[417,971,972],{"class":427},"            \u003C\u002F",[417,974,709],{"class":647},[417,976,660],{"class":427},[417,978,980,982,984],{"class":419,"line":979},36,[417,981,807],{"class":427},[417,983,358],{"class":647},[417,985,660],{"class":427},[417,987,989],{"class":419,"line":988},37,[417,990,991],{"class":427},"        ))}\n",[417,993,995],{"class":419,"line":994},38,[417,996,453],{"emptyLinePlaceholder":452},[417,998,1000,1002,1004],{"class":419,"line":999},39,[417,1001,684],{"class":427},[417,1003,358],{"class":647},[417,1005,660],{"class":427},[417,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025,1027,1030,1032,1034,1036,1038,1040,1042,1044,1046],{"class":419,"line":1008},40,[417,1010,706],{"class":427},[417,1012,709],{"class":647},[417,1014,712],{"class":462},[417,1016,654],{"class":434},[417,1018,717],{"class":423},[417,1020,720],{"class":462},[417,1022,654],{"class":434},[417,1024,725],{"class":427},[417,1026,728],{"class":434},[417,1028,1029],{"class":427}," pageCount} ",[417,1031,736],{"class":462},[417,1033,654],{"class":434},[417,1035,741],{"class":427},[417,1037,521],{"class":434},[417,1039,746],{"class":462},[417,1041,749],{"class":427},[417,1043,611],{"class":434},[417,1045,614],{"class":482},[417,1047,757],{"class":427},[417,1049,1051,1053,1055,1057,1059,1061,1064,1066],{"class":419,"line":1050},41,[417,1052,763],{"class":427},[417,1054,417],{"class":647},[417,1056,768],{"class":462},[417,1058,654],{"class":434},[417,1060,773],{"class":423},[417,1062,1063],{"class":427},">›\u003C\u002F",[417,1065,417],{"class":647},[417,1067,660],{"class":427},[417,1069,1071,1073,1075,1077,1079,1081,1084,1086],{"class":419,"line":1070},42,[417,1072,763],{"class":427},[417,1074,417],{"class":647},[417,1076,671],{"class":462},[417,1078,654],{"class":434},[417,1080,794],{"class":423},[417,1082,1083],{"class":427},">Next page\u003C\u002F",[417,1085,417],{"class":647},[417,1087,660],{"class":427},[417,1089,1091,1093,1095],{"class":419,"line":1090},43,[417,1092,807],{"class":427},[417,1094,709],{"class":647},[417,1096,660],{"class":427},[417,1098,1100,1102,1104],{"class":419,"line":1099},44,[417,1101,817],{"class":427},[417,1103,358],{"class":647},[417,1105,660],{"class":427},[417,1107,1109,1112,1114],{"class":419,"line":1108},45,[417,1110,1111],{"class":427},"      \u003C\u002F",[417,1113,355],{"class":647},[417,1115,660],{"class":427},[417,1117,1119,1122,1124],{"class":419,"line":1118},46,[417,1120,1121],{"class":427},"    \u003C\u002F",[417,1123,648],{"class":647},[417,1125,660],{"class":427},[417,1127,1129],{"class":419,"line":1128},47,[417,1130,1131],{"class":427},"  );\n",[417,1133,1135],{"class":419,"line":1134},48,[417,1136,700],{"class":427},[324,1138,1139,1140,1143,1144,1147],{},"The prev\u002Fnext buttons use a visible glyph wrapped in ",[328,1141,1142],{},"aria-hidden=\"true\""," plus an ",[328,1145,1146],{},"sr-only"," text label, so sighted users see a chevron and screen reader users hear \"Previous page\" rather than an ambiguous \"‹\".",[381,1149],{},[384,1151,1153,1155],{"id":1152},"aria-currentpage-on-the-active-page",[328,1154,338],{}," on the Active Page",[324,1157,1158,1159,1162,1163,1165],{},"The active page must be conveyed to assistive technology, not just rendered in a different color—relying on color alone is both a ",[328,1160,1161],{},"4.1.2"," failure and a contrast concern. ",[328,1164,338],{}," is the correct mechanism: it marks exactly one element in a set as the current item, and screen readers announce it as \"current page.\"",[324,1167,1168,1169,1172,1173,1176],{},"The pattern enforces the single-active-item rule automatically because ",[328,1170,1171],{},"aria-current"," is derived from ",[328,1174,1175],{},"p === page","—only one button can match. Style the current page from the same attribute so the visual and programmatic states never drift apart:",[408,1178,1182],{"className":1179,"code":1180,"language":1181,"meta":413,"style":413},"language-css shiki shiki-themes github-light github-dark",".pagination-list [aria-current='page'] {\n  font-weight: 700;\n  \u002F* Pair the visual cue with the attribute so they can never disagree *\u002F\n  outline: 2px solid var(--primary-strong);\n}\n","css",[328,1183,1184,1202,1215,1220,1247],{"__ignoreMap":413},[417,1185,1186,1189,1192,1194,1196,1199],{"class":419,"line":420},[417,1187,1188],{"class":462},".pagination-list",[417,1190,1191],{"class":427}," [",[417,1193,1171],{"class":462},[417,1195,654],{"class":434},[417,1197,1198],{"class":423},"'page'",[417,1200,1201],{"class":427},"] {\n",[417,1203,1204,1207,1210,1213],{"class":419,"line":431},[417,1205,1206],{"class":482},"  font-weight",[417,1208,1209],{"class":427},": ",[417,1211,1212],{"class":482},"700",[417,1214,428],{"class":427},[417,1216,1217],{"class":419,"line":449},[417,1218,1219],{"class":637},"  \u002F* Pair the visual cue with the attribute so they can never disagree *\u002F\n",[417,1221,1222,1225,1227,1230,1233,1236,1239,1242,1245],{"class":419,"line":456},[417,1223,1224],{"class":482},"  outline",[417,1226,1209],{"class":427},[417,1228,1229],{"class":482},"2",[417,1231,1232],{"class":434},"px",[417,1234,1235],{"class":482}," solid",[417,1237,1238],{"class":482}," var",[417,1240,1241],{"class":427},"(",[417,1243,1244],{"class":475},"--primary-strong",[417,1246,617],{"class":427},[417,1248,1249],{"class":419,"line":472},[417,1250,700],{"class":427},[324,1252,1253,1254,1257,1258,1261],{},"Driving the highlight off ",[328,1255,1256],{},"[aria-current='page']"," rather than a separate ",[328,1259,1260],{},".active"," class means there is no second source of truth to forget to update.",[381,1263],{},[384,1265,1267],{"id":1266},"accessible-prevnext-with-a-disabled-state","Accessible Prev\u002FNext With a Disabled State",[324,1269,1270,1271,1274,1275,1278],{},"At the first page, \"Previous\" has nowhere to go; at the last, \"Next\" does. Communicate that boundary with the native ",[328,1272,1273],{},"disabled"," attribute on the ",[328,1276,1277],{},"\u003Cbutton>",". A disabled native button is removed from the tab order and announced as unavailable, which is the behavior keyboard and screen reader users expect.",[324,1280,1281,1282,1285,1286,1288],{},"Avoid two common anti-patterns. First, do not hide the boundary control entirely—removing \"Previous\" on page one shifts the layout and surprises users who expect a stable control set. Second, do not fake disabling with ",[328,1283,1284],{},"aria-disabled=\"true\""," while leaving the button clickable unless you also intercept and ignore activation; a control that announces \"dimmed\" but still fires is worse than either honest state. The native ",[328,1287,1273],{}," attribute gives you the correct behavior with no extra code.",[381,1290],{},[384,1292,1294],{"id":1293},"announcing-the-visible-row-range","Announcing the Visible Row Range",[324,1296,1297,1298,1300],{},"This is the piece most pagination implementations miss. When a user clicks \"Next,\" the table's rows swap, but nothing about that change is announced—focus typically remains on the page button, so a screen reader user has no signal that the data updated. A polite live region announcing the new range is the direct remedy and a model ",[328,1299,373],{}," case: it reports a state change without moving focus or interrupting speech.",[408,1302,1304],{"className":410,"code":1303,"language":412,"meta":413,"style":413},"export function PaginatedTable({ rows, pageSize = 20 }: { rows: Row[]; pageSize?: number }) {\n  const [page, setPage] = useState(1);\n  const pageCount = Math.ceil(rows.length \u002F pageSize);\n\n  const start = (page - 1) * pageSize;\n  const visible = rows.slice(start, start + pageSize);\n  const from = rows.length === 0 ? 0 : start + 1;\n  const to = start + visible.length;\n\n  return (\n    \u003Csection aria-labelledby=\"results-heading\">\n      \u003Ch2 id=\"results-heading\">Search results\u003C\u002Fh2>\n\n      \u003Ctable>\n        \u003Ccaption>Results, page {page} of {pageCount}\u003C\u002Fcaption>\n        {\u002F* …thead and tbody render `visible`… *\u002F}\n      \u003C\u002Ftable>\n\n      \u003CPagination page={page} pageCount={pageCount} onChange={setPage} \u002F>\n\n      {\u002F* Polite status: announced after each page change, no focus move *\u002F}\n      \u003Cp role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        Showing {from}–{to} of {rows.length} results\n      \u003C\u002Fp>\n    \u003C\u002Fsection>\n  );\n}\n",[328,1305,1306,1357,1385,1412,1416,1440,1462,1497,1517,1521,1527,1544,1564,1568,1577,1591,1601,1609,1613,1642,1646,1656,1686,1696,1704,1712,1716],{"__ignoreMap":413},[417,1307,1308,1310,1312,1315,1317,1320,1322,1325,1327,1330,1332,1334,1337,1339,1341,1344,1347,1349,1352,1354],{"class":419,"line":420},[417,1309,543],{"class":434},[417,1311,546],{"class":434},[417,1313,1314],{"class":462}," PaginatedTable",[417,1316,552],{"class":427},[417,1318,1319],{"class":475},"rows",[417,1321,335],{"class":427},[417,1323,1324],{"class":475},"pageSize",[417,1326,466],{"class":434},[417,1328,1329],{"class":482}," 20",[417,1331,567],{"class":427},[417,1333,479],{"class":434},[417,1335,1336],{"class":427}," { ",[417,1338,1319],{"class":475},[417,1340,479],{"class":434},[417,1342,1343],{"class":462}," Row",[417,1345,1346],{"class":427},"[]; ",[417,1348,1324],{"class":475},[417,1350,1351],{"class":434},"?:",[417,1353,483],{"class":482},[417,1355,1356],{"class":427}," }) {\n",[417,1358,1359,1361,1363,1365,1367,1370,1373,1375,1378,1380,1383],{"class":419,"line":431},[417,1360,580],{"class":434},[417,1362,1191],{"class":427},[417,1364,511],{"class":482},[417,1366,335],{"class":427},[417,1368,1369],{"class":482},"setPage",[417,1371,1372],{"class":427},"] ",[417,1374,654],{"class":434},[417,1376,1377],{"class":462}," useState",[417,1379,1241],{"class":427},[417,1381,1382],{"class":482},"1",[417,1384,617],{"class":427},[417,1386,1387,1389,1392,1394,1397,1400,1403,1406,1409],{"class":419,"line":449},[417,1388,580],{"class":434},[417,1390,1391],{"class":482}," pageCount",[417,1393,466],{"class":434},[417,1395,1396],{"class":427}," Math.",[417,1398,1399],{"class":462},"ceil",[417,1401,1402],{"class":427},"(rows.",[417,1404,1405],{"class":482},"length",[417,1407,1408],{"class":434}," \u002F",[417,1410,1411],{"class":427}," pageSize);\n",[417,1413,1414],{"class":419,"line":456},[417,1415,453],{"emptyLinePlaceholder":452},[417,1417,1418,1420,1423,1425,1428,1430,1432,1434,1437],{"class":419,"line":472},[417,1419,580],{"class":434},[417,1421,1422],{"class":482}," start",[417,1424,466],{"class":434},[417,1426,1427],{"class":427}," (page ",[417,1429,752],{"class":434},[417,1431,614],{"class":482},[417,1433,518],{"class":427},[417,1435,1436],{"class":434},"*",[417,1438,1439],{"class":427}," pageSize;\n",[417,1441,1442,1444,1447,1449,1452,1455,1458,1460],{"class":419,"line":488},[417,1443,580],{"class":434},[417,1445,1446],{"class":482}," visible",[417,1448,466],{"class":434},[417,1450,1451],{"class":427}," rows.",[417,1453,1454],{"class":462},"slice",[417,1456,1457],{"class":427},"(start, start ",[417,1459,611],{"class":434},[417,1461,1411],{"class":427},[417,1463,1464,1466,1469,1471,1473,1475,1478,1481,1484,1486,1488,1491,1493,1495],{"class":419,"line":500},[417,1465,580],{"class":434},[417,1467,1468],{"class":482}," from",[417,1470,466],{"class":434},[417,1472,1451],{"class":427},[417,1474,1405],{"class":482},[417,1476,1477],{"class":434}," ===",[417,1479,1480],{"class":482}," 0",[417,1482,1483],{"class":434}," ?",[417,1485,1480],{"class":482},[417,1487,911],{"class":434},[417,1489,1490],{"class":427}," start ",[417,1492,611],{"class":434},[417,1494,614],{"class":482},[417,1496,428],{"class":427},[417,1498,1499,1501,1504,1506,1508,1510,1513,1515],{"class":419,"line":529},[417,1500,580],{"class":434},[417,1502,1503],{"class":482}," to",[417,1505,466],{"class":434},[417,1507,1490],{"class":427},[417,1509,611],{"class":434},[417,1511,1512],{"class":427}," visible.",[417,1514,1405],{"class":482},[417,1516,428],{"class":427},[417,1518,1519],{"class":419,"line":535},[417,1520,453],{"emptyLinePlaceholder":452},[417,1522,1523,1525],{"class":419,"line":540},[417,1524,628],{"class":434},[417,1526,631],{"class":427},[417,1528,1529,1531,1534,1537,1539,1542],{"class":419,"line":577},[417,1530,644],{"class":427},[417,1532,1533],{"class":647},"section",[417,1535,1536],{"class":462}," aria-labelledby",[417,1538,654],{"class":434},[417,1540,1541],{"class":423},"\"results-heading\"",[417,1543,660],{"class":427},[417,1545,1546,1548,1550,1553,1555,1557,1560,1562],{"class":419,"line":620},[417,1547,666],{"class":427},[417,1549,384],{"class":647},[417,1551,1552],{"class":462}," id",[417,1554,654],{"class":434},[417,1556,1541],{"class":423},[417,1558,1559],{"class":427},">Search results\u003C\u002F",[417,1561,384],{"class":647},[417,1563,660],{"class":427},[417,1565,1566],{"class":419,"line":625},[417,1567,453],{"emptyLinePlaceholder":452},[417,1569,1570,1572,1575],{"class":419,"line":634},[417,1571,666],{"class":427},[417,1573,1574],{"class":647},"table",[417,1576,660],{"class":427},[417,1578,1579,1581,1584,1587,1589],{"class":419,"line":641},[417,1580,684],{"class":427},[417,1582,1583],{"class":647},"caption",[417,1585,1586],{"class":427},">Results, page {page} of {pageCount}\u003C\u002F",[417,1588,1583],{"class":647},[417,1590,660],{"class":427},[417,1592,1593,1596,1599],{"class":419,"line":663},[417,1594,1595],{"class":427},"        {",[417,1597,1598],{"class":637},"\u002F* …thead and tbody render `visible`… *\u002F",[417,1600,700],{"class":427},[417,1602,1603,1605,1607],{"class":419,"line":681},[417,1604,1111],{"class":427},[417,1606,1574],{"class":647},[417,1608,660],{"class":427},[417,1610,1611],{"class":419,"line":691},[417,1612,453],{"emptyLinePlaceholder":452},[417,1614,1615,1617,1620,1623,1625,1628,1630,1632,1635,1637,1639],{"class":419,"line":703},[417,1616,666],{"class":427},[417,1618,1619],{"class":482},"Pagination",[417,1621,1622],{"class":462}," page",[417,1624,654],{"class":434},[417,1626,1627],{"class":427},"{page} ",[417,1629,559],{"class":462},[417,1631,654],{"class":434},[417,1633,1634],{"class":427},"{pageCount} ",[417,1636,564],{"class":462},[417,1638,654],{"class":434},[417,1640,1641],{"class":427},"{setPage} \u002F>\n",[417,1643,1644],{"class":419,"line":760},[417,1645,453],{"emptyLinePlaceholder":452},[417,1647,1648,1651,1654],{"class":419,"line":783},[417,1649,1650],{"class":427},"      {",[417,1652,1653],{"class":637},"\u002F* Polite status: announced after each page change, no focus move *\u002F",[417,1655,700],{"class":427},[417,1657,1658,1660,1662,1665,1667,1670,1673,1675,1678,1680,1682,1684],{"class":419,"line":804},[417,1659,666],{"class":427},[417,1661,324],{"class":647},[417,1663,1664],{"class":462}," role",[417,1666,654],{"class":434},[417,1668,1669],{"class":423},"\"status\"",[417,1671,1672],{"class":462}," aria-live",[417,1674,654],{"class":434},[417,1676,1677],{"class":423},"\"polite\"",[417,1679,671],{"class":462},[417,1681,654],{"class":434},[417,1683,794],{"class":423},[417,1685,660],{"class":427},[417,1687,1688,1691,1693],{"class":419,"line":814},[417,1689,1690],{"class":427},"        Showing {from}–{to} of {rows.",[417,1692,1405],{"class":482},[417,1694,1695],{"class":427},"} results\n",[417,1697,1698,1700,1702],{"class":419,"line":824},[417,1699,1111],{"class":427},[417,1701,324],{"class":647},[417,1703,660],{"class":427},[417,1705,1706,1708,1710],{"class":419,"line":829},[417,1707,1121],{"class":427},[417,1709,1533],{"class":647},[417,1711,660],{"class":427},[417,1713,1714],{"class":419,"line":849},[417,1715,1131],{"class":427},[417,1717,1718],{"class":419,"line":864},[417,1719,700],{"class":427},[324,1721,1722,1723,1726],{},"Two details make the announcement reliable. Render the ",[328,1724,1725],{},"role=\"status\""," region on initial mount (empty), not conditionally, so it exists in the accessibility tree before its first update—live regions added at the same time as their content are frequently missed. And keep the wording stable (\"Showing X–Y of N results\") so repeated page turns produce a predictable, scannable message rather than a different sentence each time.",[381,1728],{},[384,1730,1732],{"id":1731},"keyboard-focus-handling-after-a-page-change","Keyboard Focus Handling After a Page Change",[324,1734,1735,1736,479],{},"After a page change, where focus lands determines whether keyboard users can continue working or get stranded. There are two sound strategies; pick one and apply it consistently to honor ",[328,1737,379],{},[355,1739,1740,1749],{},[358,1741,1742,1745,1746,1748],{},[351,1743,1744],{},"Keep focus on the activated control."," When the user clicks page \"3,\" leave focus on the page-3 button (now marked ",[328,1747,1171],{},"). This is the least disruptive choice for number buttons and works well when the live region announces the new range.",[358,1750,1751,1754,1755,1757,1758,1761],{},[351,1752,1753],{},"Move focus to the table or its heading."," For prev\u002Fnext—or when the boundary button you were on becomes ",[328,1756,1273],{}," and thus loses focus—programmatically move focus to the results heading or the table's container (with ",[328,1759,1760],{},"tabIndex={-1}",") so the user lands on the refreshed data rather than being dumped to the top of the document.",[408,1763,1765],{"className":410,"code":1764,"language":412,"meta":413,"style":413},"function handleChange(next: number) {\n  setPage(next);\n  \u002F\u002F If the control you used just became disabled, focus the results region\n  if (next === 1 || next === pageCount) {\n    requestAnimationFrame(() => headingRef.current?.focus());\n  }\n}\n",[328,1766,1767,1786,1794,1799,1822,1841,1846],{"__ignoreMap":413},[417,1768,1769,1772,1775,1777,1780,1782,1784],{"class":419,"line":420},[417,1770,1771],{"class":434},"function",[417,1773,1774],{"class":462}," handleChange",[417,1776,1241],{"class":427},[417,1778,1779],{"class":475},"next",[417,1781,479],{"class":434},[417,1783,483],{"class":482},[417,1785,574],{"class":427},[417,1787,1788,1791],{"class":419,"line":431},[417,1789,1790],{"class":462},"  setPage",[417,1792,1793],{"class":427},"(next);\n",[417,1795,1796],{"class":419,"line":449},[417,1797,1798],{"class":637},"  \u002F\u002F If the control you used just became disabled, focus the results region\n",[417,1800,1801,1804,1807,1809,1811,1814,1817,1819],{"class":419,"line":456},[417,1802,1803],{"class":434},"  if",[417,1805,1806],{"class":427}," (next ",[417,1808,728],{"class":434},[417,1810,614],{"class":482},[417,1812,1813],{"class":434}," ||",[417,1815,1816],{"class":427}," next ",[417,1818,728],{"class":434},[417,1820,1821],{"class":427}," pageCount) {\n",[417,1823,1824,1827,1830,1832,1835,1838],{"class":419,"line":472},[417,1825,1826],{"class":462},"    requestAnimationFrame",[417,1828,1829],{"class":427},"(() ",[417,1831,521],{"class":434},[417,1833,1834],{"class":427}," headingRef.current?.",[417,1836,1837],{"class":462},"focus",[417,1839,1840],{"class":427},"());\n",[417,1842,1843],{"class":419,"line":488},[417,1844,1845],{"class":427},"  }\n",[417,1847,1848],{"class":419,"line":500},[417,1849,700],{"class":427},[324,1851,1852,1853,1855,1856,1859],{},"The failure to avoid is silent focus loss: when a button becomes ",[328,1854,1273],{}," after you activate it, the browser drops focus to ",[328,1857,1858],{},"\u003Cbody>",", sending keyboard users back to the top of the page. Detect that case and redirect focus deliberately.",[381,1861],{},[384,1863,1865],{"id":1864},"how-to-verify","How to Verify",[355,1867,1868,1880,1889,1895],{},[358,1869,1870,1873,1874,1876,1877,1879],{},[351,1871,1872],{},"Automated (axe-core \u002F jest-axe):"," Assert no violations. axe verifies the ",[328,1875,334],{}," has an accessible name and flags an invalid ",[328,1878,1171],{}," value, but it cannot confirm the range announcement fires—test that manually or by asserting the live region's text content after a simulated click.",[358,1881,1882,1885,1886,1888],{},[351,1883,1884],{},"Keyboard only:"," Tab into the pagination nav. Confirm every page button and prev\u002Fnext is reachable, that disabled boundary buttons are skipped, and that Enter\u002FSpace activate a page. After changing pages, confirm focus is somewhere sensible—on the activated button or the results region—never lost to ",[328,1887,1858],{},".",[358,1890,1891,1894],{},[351,1892,1893],{},"Screen reader (NVDA + Firefox, VoiceOver + Safari):"," Confirm the landmark is announced as \"Pagination navigation,\" that the active page is read as \"current page,\" and that after each page change the polite region speaks \"Showing X–Y of N results\" without you moving focus.",[358,1896,1897,1900],{},[351,1898,1899],{},"Boundary check:"," Navigate to the first and last pages and confirm the appropriate prev\u002Fnext button is announced as unavailable and is not focusable.",[1902,1903,1904],"blockquote",{},[324,1905,1906,1909,1910,1912,1913,1915],{},[351,1907,1908],{},"Testing Hook:"," In a component test, simulate clicking \"Next,\" then assert that exactly one button has ",[328,1911,338],{},", the previously disabled state updated, and the ",[328,1914,1725],{}," region's text content reads the expected \"Showing X–Y of N results.\"",[381,1917],{},[384,1919,1921],{"id":1920},"common-a11y-mistakes","Common a11y Mistakes",[355,1923,1924,1936,1947,1956,1962,1972],{},[358,1925,1926,1935],{},[351,1927,1928,1929,1931,1932,1934],{},"Building pagination from ",[328,1930,330],{},"s or unlabeled ",[328,1933,334],{},"s",", so it is neither a recognizable landmark nor distinguishable from other navigation.",[358,1937,1938,1941,1942,1944,1945,1888],{},[351,1939,1940],{},"Indicating the active page with color only",", failing ",[328,1943,1161],{}," and leaving the current page invisible to assistive technology—use ",[328,1946,338],{},[358,1948,1949,1955],{},[351,1950,1951,1952,1954],{},"Setting ",[328,1953,1171],{}," on more than one control",", so the current page is ambiguous.",[358,1957,1958,1961],{},[351,1959,1960],{},"Turning pages with no live region",", leaving screen reader users unaware the rows changed because focus stayed on the button.",[358,1963,1964,1971],{},[351,1965,1966,1967,1970],{},"Faking a disabled state with ",[328,1968,1969],{},"aria-disabled"," while leaving the button clickable",", or hiding boundary controls entirely and shifting the layout.",[358,1973,1974,1979],{},[351,1975,1976,1977],{},"Letting focus drop to ",[328,1978,1858],{}," when the activated button becomes disabled, sending keyboard users to the top of the page.",[381,1981],{},[384,1983,1985],{"id":1984},"frequently-asked-questions","Frequently Asked Questions",[324,1987,1988,1994,1995,1997,1998,2000,2001,2004,2005,1888],{},[351,1989,1990,1991,1993],{},"Why does pagination need to be a ",[328,1992,334],{}," instead of just a list of buttons?","\nPagination is a navigation mechanism, and wrapping it in a ",[328,1996,334],{}," exposes it as a landmark that assistive technology users can jump to directly. Because a page usually has several ",[328,1999,334],{}," landmarks, give the pagination one an ",[328,2002,2003],{},"aria-label=\"Pagination\""," so it is distinguishable, satisfying ",[328,2006,362],{},[324,2008,2009,2015,2016,1888],{},[351,2010,2011,2012,2014],{},"Is ",[328,2013,338],{}," enough to tell users which page they are on?","\nIt is the correct way to expose the active page, and screen readers announce it as \"current page\" when the user reaches that control. But it does not tell users that the table's rows changed when they navigate. Pair it with a polite live region announcing the new visible range so the data change is also communicated, satisfying ",[328,2017,373],{},[324,2019,2020,2023,2024,1274,2026,2028,2029,2031],{},[351,2021,2022],{},"How should I disable Previous on the first page and Next on the last?","\nUse the native ",[328,2025,1273],{},[328,2027,1277],{},". A disabled native button is removed from the tab order and announced as unavailable, which matches user expectations. Avoid hiding the control, which shifts layout, and avoid ",[328,2030,1969],{}," on a still-clickable button, which announces \"dimmed\" while remaining operable.",[324,2033,2034,2037,2038,2040,2041,2043],{},[351,2035,2036],{},"Where should focus go after a user changes pages?","\nEither keep focus on the activated page button or move it to the results heading or table container. Choose one and apply it consistently to honor ",[328,2039,379],{},". The critical thing to avoid is silent focus loss: when the button you activated becomes disabled, the browser drops focus to ",[328,2042,1858],{},", so detect that case and redirect focus to the refreshed results.",[381,2045],{},[384,2047,2049],{"id":2048},"related-guides","Related guides",[355,2051,2052,2056,2061],{},[358,2053,2054],{},[344,2055,109],{"href":346},[358,2057,2058],{},[344,2059,121],{"href":2060},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002F",[358,2062,2063],{},[344,2064,127],{"href":2065},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fvirtualizing-long-lists-accessibly-in-react\u002F",[2067,2068,2069],"style",{},"html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":413,"searchDepth":431,"depth":431,"links":2071},[2072,2074,2076,2077,2078,2079,2080,2081,2082],{"id":386,"depth":431,"text":2073},"A Labeled \u003Cnav> for the Pagination Control",{"id":1152,"depth":431,"text":2075},"aria-current=\"page\" on the Active Page",{"id":1266,"depth":431,"text":1267},{"id":1293,"depth":431,"text":1294},{"id":1731,"depth":431,"text":1732},{"id":1864,"depth":431,"text":1865},{"id":1920,"depth":431,"text":1921},{"id":1984,"depth":431,"text":1985},{"id":2048,"depth":431,"text":2049},null,"Build pagination React screen reader users can follow—a labeled nav, aria-current on the active page, and a live announcement of the visible row range after each change.","md",{},false,{"title":115,"description":2084},"qBuc5b_TNwHbRrdiXcbK1b7NQB3hch4M1VQk_KztFCE",[2091,2130,2131,2194],{"title":5,"path":6,"stem":7,"children":2092},[2093,2094,2097,2100,2106,2112,2121,2127],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2095},[2096],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2098},[2099],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2101},[2102,2103],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2104},[2105],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2107},[2108,2109],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2110},[2111],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2113},[2114,2115,2118],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2116},[2117],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2119},[2120],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2122},[2123,2124],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2125},[2126],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2128},[2129],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2132},[2133,2134,2140,2152,2164,2167,2176,2188],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2135},[2136,2137],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2138},[2139],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2141},[2142,2143,2146,2149],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2144},[2145],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2147},[2148],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2150},[2151],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2153},[2154,2155,2158,2161],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2156},[2157],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2159},[2160],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2162},[2163],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2165},[2166],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2168},[2169,2170,2173],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2171},[2172],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2174},[2175],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2177},[2178,2179,2182,2185],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2180},[2181],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2183},[2184],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2186},[2187],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2189},[2190,2191],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2192},[2193],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2195},[2196,2197,2206,2215,2224,2233],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2198},[2199,2200,2203],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2201},[2202],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2204},[2205],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2207},[2208,2209,2212],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2210},[2211],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2213},[2214],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2216},[2217,2218,2221],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2219},[2220],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2222},[2223],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2225},[2226,2227,2230],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2228},[2229],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2231},[2232],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2234},[2235,2236,2239],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2237},[2238],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2240},[2241],{"title":309,"path":310,"stem":311},1781785524109]