[{"data":1,"prerenderedAt":2475},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002F":314,"content-navigation":2323},[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":121,"body":316,"date":2316,"description":2317,"extension":2318,"image":2316,"meta":2319,"modifiedAt":2316,"navigation":523,"noindex":2320,"path":122,"publishedAt":2316,"seo":2321,"stem":123,"updatedAt":2316,"__hash__":2322},"content\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Fbuilding-a-sortable-accessible-data-table-in-react\u002Findex.md",{"type":317,"value":318,"toc":2303},"minimark",[319,323,336,356,362,388,391,396,399,439,442,444,451,478,1082,1092,1094,1100,1126,1142,1964,1966,1970,1982,1997,2007,2009,2013,2021,2048,2055,2057,2061,2064,2120,2138,2140,2144,2206,2208,2212,2239,2254,2272,2278,2280,2284,2299],[320,321,121],"h1",{"id":322},"building-a-sortable-accessible-data-table-in-react",[324,325,326,327,331,332,335],"p",{},"Column sorting is the feature that most often turns an otherwise accessible table into a barrier. A header that responds to clicks but is built from a bare ",[328,329,330],"code",{},"\u003Cth onClick>"," is invisible to keyboard users; a sort that reorders rows without exposing the new state or announcing the change leaves screen reader users staring at data that silently rearranged. This guide builds a sortable table that gets all three details right: a real button as the control, ",[328,333,334],{},"aria-sort"," on the active header, and a polite announcement of the new order.",[324,337,338,339,343,344,347,348,351,352,355],{},"This is a deep dive under ",[340,341,109],"a",{"href":342},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002F",". If you have not yet established semantic table markup, start there—this guide assumes a working ",[328,345,346],{},"\u003Ctable>"," with ",[328,349,350],{},"\u003Ccaption>"," and ",[328,353,354],{},"\u003Cth scope>"," and focuses solely on making the sort interaction accessible.",[324,357,358],{},[359,360,361],"strong",{},"Mapped WCAG Success Criteria:",[363,364,365,372,377,383],"ul",{},[366,367,368,371],"li",{},[328,369,370],{},"2.1.1 Keyboard"," (Level A)",[366,373,374,371],{},[328,375,376],{},"4.1.2 Name, Role, Value",[366,378,379,382],{},[328,380,381],{},"4.1.3 Status Messages"," (Level AA)",[366,384,385,371],{},[328,386,387],{},"1.3.1 Info and Relationships",[389,390],"hr",{},[392,393,395],"h2",{"id":394},"prerequisites","Prerequisites",[324,397,398],{},"This pattern assumes you already have:",[363,400,401,421,432],{},[366,402,403,404,406,407,409,410,413,414,417,418,420],{},"A native ",[328,405,346],{}," with a ",[328,408,350],{},", ",[328,411,412],{},"\u003Cthead>",", and ",[328,415,416],{},"\u003Cth scope=\"col\">"," headers, so the column-to-cell relationships required by ",[328,419,387],{}," are in place.",[366,422,423,424,427,428,431],{},"A way to render rows from sorted application state—",[328,425,426],{},"useState"," plus ",[328,429,430],{},"useMemo"," here, but the pattern is identical with TanStack Table or any data layer.",[366,433,434,435,438],{},"A polite live region available in the DOM. We render one inline below, but a shared announcer like the one in ",[340,436,133],{"href":437},"\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002F"," works just as well.",[324,440,441],{},"The goal is to add sorting without touching any of the markup that makes the table itself accessible.",[389,443],{},[392,445,447,448],{"id":446},"a-sortable-header-is-a-button-inside-a-th","A Sortable Header Is a Button Inside a ",[328,449,450],{},"\u003Cth>",[324,452,453,454,457,458,462,463,465,466,469,470,351,472,474,475,477],{},"The control that triggers a sort must be a real ",[328,455,456],{},"\u003Cbutton>",", and it must live ",[459,460,461],"em",{},"inside"," the ",[328,464,450],{},", not replace it. This keeps the cell's role as a column header (",[328,467,468],{},"1.3.1",") while making the trigger keyboard-operable and correctly exposed as a button per ",[328,471,370],{},[328,473,376],{},". A ",[328,476,330],{}," gives you neither—it is not focusable, not in the tab order, and announced as a plain header with no hint that it does anything.",[479,480,485],"pre",{"className":481,"code":482,"language":483,"meta":484,"style":484},"language-tsx shiki shiki-themes github-light github-dark","'use client';\nimport { useMemo, useState } from 'react';\n\ntype Dir = 'ascending' | 'descending' | 'none';\ntype SortState = { key: keyof Row; dir: Exclude\u003CDir, 'none'> };\n\nconst COLUMNS: { key: keyof Row; label: string }[] = [\n  { key: 'region', label: 'Region' },\n  { key: 'revenue', label: 'Revenue' },\n  { key: 'growth', label: 'Growth' },\n];\n\nfunction SortableHeader({\n  column, sort, onSort,\n}: {\n  column: { key: keyof Row; label: string };\n  sort: SortState;\n  onSort: (key: keyof Row) => void;\n}) {\n  const isActive = sort.key === column.key;\n  \u002F\u002F aria-sort belongs on the th so AT reports it as a property of the column\n  const ariaSort: Dir = isActive ? sort.dir : 'none';\n\n  return (\n    \u003Cth scope=\"col\" aria-sort={ariaSort}>\n      {\u002F* Real button: focusable, in tab order, announced as a button *\u002F}\n      \u003Cbutton type=\"button\" onClick={() => onSort(column.key)}>\n        {column.label}\n        {\u002F* Arrow is purely visual; aria-sort already conveys the direction *\u002F}\n        \u003Cspan aria-hidden=\"true\">\n          {isActive ? (sort.dir === 'ascending' ? ' ▲' : ' ▼') : ''}\n        \u003C\u002Fspan>\n      \u003C\u002Fbutton>\n    \u003C\u002Fth>\n  );\n}\n","tsx","",[328,486,487,500,518,525,554,605,610,651,669,684,699,705,710,722,741,752,780,792,822,828,848,855,884,889,898,924,936,969,975,986,1005,1041,1051,1061,1071,1077],{"__ignoreMap":484},[488,489,492,496],"span",{"class":490,"line":491},"line",1,[488,493,495],{"class":494},"sZZnC","'use client'",[488,497,499],{"class":498},"sVt8B",";\n",[488,501,503,507,510,513,516],{"class":490,"line":502},2,[488,504,506],{"class":505},"szBVR","import",[488,508,509],{"class":498}," { useMemo, useState } ",[488,511,512],{"class":505},"from",[488,514,515],{"class":494}," 'react'",[488,517,499],{"class":498},[488,519,521],{"class":490,"line":520},3,[488,522,524],{"emptyLinePlaceholder":523},true,"\n",[488,526,528,531,535,538,541,544,547,549,552],{"class":490,"line":527},4,[488,529,530],{"class":505},"type",[488,532,534],{"class":533},"sScJk"," Dir",[488,536,537],{"class":505}," =",[488,539,540],{"class":494}," 'ascending'",[488,542,543],{"class":505}," |",[488,545,546],{"class":494}," 'descending'",[488,548,543],{"class":505},[488,550,551],{"class":494}," 'none'",[488,553,499],{"class":498},[488,555,557,559,562,564,567,571,574,577,580,583,586,588,591,594,597,599,602],{"class":490,"line":556},5,[488,558,530],{"class":505},[488,560,561],{"class":533}," SortState",[488,563,537],{"class":505},[488,565,566],{"class":498}," { ",[488,568,570],{"class":569},"s4XuR","key",[488,572,573],{"class":505},":",[488,575,576],{"class":505}," keyof",[488,578,579],{"class":533}," Row",[488,581,582],{"class":498},"; ",[488,584,585],{"class":569},"dir",[488,587,573],{"class":505},[488,589,590],{"class":533}," Exclude",[488,592,593],{"class":498},"\u003C",[488,595,596],{"class":533},"Dir",[488,598,409],{"class":498},[488,600,601],{"class":494},"'none'",[488,603,604],{"class":498},"> };\n",[488,606,608],{"class":490,"line":607},6,[488,609,524],{"emptyLinePlaceholder":523},[488,611,613,616,620,622,624,626,628,630,632,634,637,639,642,645,648],{"class":490,"line":612},7,[488,614,615],{"class":505},"const",[488,617,619],{"class":618},"sj4cs"," COLUMNS",[488,621,573],{"class":505},[488,623,566],{"class":498},[488,625,570],{"class":569},[488,627,573],{"class":505},[488,629,576],{"class":505},[488,631,579],{"class":533},[488,633,582],{"class":498},[488,635,636],{"class":569},"label",[488,638,573],{"class":505},[488,640,641],{"class":618}," string",[488,643,644],{"class":498}," }[] ",[488,646,647],{"class":505},"=",[488,649,650],{"class":498}," [\n",[488,652,654,657,660,663,666],{"class":490,"line":653},8,[488,655,656],{"class":498},"  { key: ",[488,658,659],{"class":494},"'region'",[488,661,662],{"class":498},", label: ",[488,664,665],{"class":494},"'Region'",[488,667,668],{"class":498}," },\n",[488,670,672,674,677,679,682],{"class":490,"line":671},9,[488,673,656],{"class":498},[488,675,676],{"class":494},"'revenue'",[488,678,662],{"class":498},[488,680,681],{"class":494},"'Revenue'",[488,683,668],{"class":498},[488,685,687,689,692,694,697],{"class":490,"line":686},10,[488,688,656],{"class":498},[488,690,691],{"class":494},"'growth'",[488,693,662],{"class":498},[488,695,696],{"class":494},"'Growth'",[488,698,668],{"class":498},[488,700,702],{"class":490,"line":701},11,[488,703,704],{"class":498},"];\n",[488,706,708],{"class":490,"line":707},12,[488,709,524],{"emptyLinePlaceholder":523},[488,711,713,716,719],{"class":490,"line":712},13,[488,714,715],{"class":505},"function",[488,717,718],{"class":533}," SortableHeader",[488,720,721],{"class":498},"({\n",[488,723,725,728,730,733,735,738],{"class":490,"line":724},14,[488,726,727],{"class":569},"  column",[488,729,409],{"class":498},[488,731,732],{"class":569},"sort",[488,734,409],{"class":498},[488,736,737],{"class":569},"onSort",[488,739,740],{"class":498},",\n",[488,742,744,747,749],{"class":490,"line":743},15,[488,745,746],{"class":498},"}",[488,748,573],{"class":505},[488,750,751],{"class":498}," {\n",[488,753,755,757,759,761,763,765,767,769,771,773,775,777],{"class":490,"line":754},16,[488,756,727],{"class":569},[488,758,573],{"class":505},[488,760,566],{"class":498},[488,762,570],{"class":569},[488,764,573],{"class":505},[488,766,576],{"class":505},[488,768,579],{"class":533},[488,770,582],{"class":498},[488,772,636],{"class":569},[488,774,573],{"class":505},[488,776,641],{"class":618},[488,778,779],{"class":498}," };\n",[488,781,783,786,788,790],{"class":490,"line":782},17,[488,784,785],{"class":569},"  sort",[488,787,573],{"class":505},[488,789,561],{"class":533},[488,791,499],{"class":498},[488,793,795,798,800,803,805,807,809,811,814,817,820],{"class":490,"line":794},18,[488,796,797],{"class":533},"  onSort",[488,799,573],{"class":505},[488,801,802],{"class":498}," (",[488,804,570],{"class":569},[488,806,573],{"class":505},[488,808,576],{"class":505},[488,810,579],{"class":533},[488,812,813],{"class":498},") ",[488,815,816],{"class":505},"=>",[488,818,819],{"class":618}," void",[488,821,499],{"class":498},[488,823,825],{"class":490,"line":824},19,[488,826,827],{"class":498},"}) {\n",[488,829,831,834,837,839,842,845],{"class":490,"line":830},20,[488,832,833],{"class":505},"  const",[488,835,836],{"class":618}," isActive",[488,838,537],{"class":505},[488,840,841],{"class":498}," sort.key ",[488,843,844],{"class":505},"===",[488,846,847],{"class":498}," column.key;\n",[488,849,851],{"class":490,"line":850},21,[488,852,854],{"class":853},"sJ8bj","  \u002F\u002F aria-sort belongs on the th so AT reports it as a property of the column\n",[488,856,858,860,863,865,867,869,872,875,878,880,882],{"class":490,"line":857},22,[488,859,833],{"class":505},[488,861,862],{"class":618}," ariaSort",[488,864,573],{"class":505},[488,866,534],{"class":533},[488,868,537],{"class":505},[488,870,871],{"class":498}," isActive ",[488,873,874],{"class":505},"?",[488,876,877],{"class":498}," sort.dir ",[488,879,573],{"class":505},[488,881,551],{"class":494},[488,883,499],{"class":498},[488,885,887],{"class":490,"line":886},23,[488,888,524],{"emptyLinePlaceholder":523},[488,890,892,895],{"class":490,"line":891},24,[488,893,894],{"class":505},"  return",[488,896,897],{"class":498}," (\n",[488,899,901,904,908,911,913,916,919,921],{"class":490,"line":900},25,[488,902,903],{"class":498},"    \u003C",[488,905,907],{"class":906},"s9eBZ","th",[488,909,910],{"class":533}," scope",[488,912,647],{"class":505},[488,914,915],{"class":494},"\"col\"",[488,917,918],{"class":533}," aria-sort",[488,920,647],{"class":505},[488,922,923],{"class":498},"{ariaSort}>\n",[488,925,927,930,933],{"class":490,"line":926},26,[488,928,929],{"class":498},"      {",[488,931,932],{"class":853},"\u002F* Real button: focusable, in tab order, announced as a button *\u002F",[488,934,935],{"class":498},"}\n",[488,937,939,942,945,948,950,953,956,958,961,963,966],{"class":490,"line":938},27,[488,940,941],{"class":498},"      \u003C",[488,943,944],{"class":906},"button",[488,946,947],{"class":533}," type",[488,949,647],{"class":505},[488,951,952],{"class":494},"\"button\"",[488,954,955],{"class":533}," onClick",[488,957,647],{"class":505},[488,959,960],{"class":498},"{() ",[488,962,816],{"class":505},[488,964,965],{"class":533}," onSort",[488,967,968],{"class":498},"(column.key)}>\n",[488,970,972],{"class":490,"line":971},28,[488,973,974],{"class":498},"        {column.label}\n",[488,976,978,981,984],{"class":490,"line":977},29,[488,979,980],{"class":498},"        {",[488,982,983],{"class":853},"\u002F* Arrow is purely visual; aria-sort already conveys the direction *\u002F",[488,985,935],{"class":498},[488,987,989,992,994,997,999,1002],{"class":490,"line":988},30,[488,990,991],{"class":498},"        \u003C",[488,993,488],{"class":906},[488,995,996],{"class":533}," aria-hidden",[488,998,647],{"class":505},[488,1000,1001],{"class":494},"\"true\"",[488,1003,1004],{"class":498},">\n",[488,1006,1008,1011,1013,1016,1018,1020,1023,1026,1029,1032,1034,1036,1039],{"class":490,"line":1007},31,[488,1009,1010],{"class":498},"          {isActive ",[488,1012,874],{"class":505},[488,1014,1015],{"class":498}," (sort.dir ",[488,1017,844],{"class":505},[488,1019,540],{"class":494},[488,1021,1022],{"class":505}," ?",[488,1024,1025],{"class":494}," ' ▲'",[488,1027,1028],{"class":505}," :",[488,1030,1031],{"class":494}," ' ▼'",[488,1033,813],{"class":498},[488,1035,573],{"class":505},[488,1037,1038],{"class":494}," ''",[488,1040,935],{"class":498},[488,1042,1044,1047,1049],{"class":490,"line":1043},32,[488,1045,1046],{"class":498},"        \u003C\u002F",[488,1048,488],{"class":906},[488,1050,1004],{"class":498},[488,1052,1054,1057,1059],{"class":490,"line":1053},33,[488,1055,1056],{"class":498},"      \u003C\u002F",[488,1058,944],{"class":906},[488,1060,1004],{"class":498},[488,1062,1064,1067,1069],{"class":490,"line":1063},34,[488,1065,1066],{"class":498},"    \u003C\u002F",[488,1068,907],{"class":906},[488,1070,1004],{"class":498},[488,1072,1074],{"class":490,"line":1073},35,[488,1075,1076],{"class":498},"  );\n",[488,1078,1080],{"class":490,"line":1079},36,[488,1081,935],{"class":498},[324,1083,1084,1085,1088,1089,1091],{},"The visible arrow is wrapped in ",[328,1086,1087],{},"aria-hidden=\"true\""," deliberately. Direction is already exposed through ",[328,1090,334],{},"; letting a screen reader also read \"up arrow\" would be redundant and confusing. Convey state to assistive technology through ARIA, and to sighted users through the icon—never duplicate one in the other.",[389,1093],{},[392,1095,1097,1098],{"id":1096},"reflecting-direction-with-aria-sort","Reflecting Direction With ",[328,1099,334],{},[324,1101,1102,1104,1105,409,1108,409,1111,1114,1115,1118,1119,1125],{},[328,1103,334],{}," is the property that tells assistive technology how a column is currently sorted. It takes one of ",[328,1106,1107],{},"ascending",[328,1109,1110],{},"descending",[328,1112,1113],{},"none",", or ",[328,1116,1117],{},"other",", and—critically—",[359,1120,1121,1122,1124],{},"at most one header in the table may have a value other than ",[328,1123,1113],{}," at a time",". If two headers claim to be the active sort, the user cannot tell which one actually ordered the rows.",[324,1127,1128,1129,1131,1132,1134,1135,1138,1139,1141],{},"The rule is enforced naturally if you derive each header's ",[328,1130,334],{}," from a single piece of state. In the component above, only the header whose ",[328,1133,570],{}," matches ",[328,1136,1137],{},"sort.key"," receives the active direction; every other header computes ",[328,1140,601],{},". There is no separate bookkeeping to keep in sync.",[479,1143,1145],{"className":481,"code":1144,"language":483,"meta":484,"style":484},"export function SortableTable({ data }: { data: Row[] }) {\n  const [sort, setSort] = useState\u003CSortState>({ key: 'region', dir: 'ascending' });\n  const [message, setMessage] = useState('');\n\n  const sorted = useMemo(() => {\n    const copy = [...data];\n    copy.sort((a, b) => {\n      const av = a[sort.key];\n      const bv = b[sort.key];\n      const cmp = av \u003C bv ? -1 : av > bv ? 1 : 0;\n      return sort.dir === 'ascending' ? cmp : -cmp;\n    });\n    return copy;\n  }, [data, sort]);\n\n  function handleSort(key: keyof Row) {\n    setSort((prev) => {\n      \u002F\u002F Toggle direction if the same column, else start ascending\n      const dir: SortState['dir'] =\n        prev.key === key && prev.dir === 'ascending' ? 'descending' : 'ascending';\n      const label = COLUMNS.find((c) => c.key === key)!.label;\n      \u002F\u002F Announce the new order — see next section\n      setMessage(`Table sorted by ${label}, ${dir}.`);\n      return { key, dir };\n    });\n  }\n\n  return (\n    \u003C>\n      \u003Ctable>\n        \u003Ccaption>Q3 revenue by region (sortable)\u003C\u002Fcaption>\n        \u003Cthead>\n          \u003Ctr>\n            {COLUMNS.map((col) => (\n              \u003CSortableHeader key={String(col.key)} column={col} sort={sort} onSort={handleSort} \u002F>\n            ))}\n          \u003C\u002Ftr>\n        \u003C\u002Fthead>\n        \u003Ctbody>\n          {sorted.map((row) => (\n            \u003Ctr key={row.id}>\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      {\u002F* Polite live region: announces sort changes without stealing focus *\u002F}\n      \u003Cdiv role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {message}\n      \u003C\u002Fdiv>\n    \u003C\u002F>\n  );\n}\n",[328,1146,1147,1180,1221,1250,1254,1273,1291,1314,1327,1339,1385,1408,1413,1421,1426,1430,1451,1467,1472,1494,1524,1564,1569,1591,1598,1602,1607,1611,1617,1622,1631,1645,1654,1664,1688,1732,1737,1747,1756,1766,1785,1800,1821,1836,1850,1860,1866,1875,1884,1889,1899,1933,1939,1948,1954,1959],{"__ignoreMap":484},[488,1148,1149,1152,1155,1158,1161,1164,1167,1169,1171,1173,1175,1177],{"class":490,"line":491},[488,1150,1151],{"class":505},"export",[488,1153,1154],{"class":505}," function",[488,1156,1157],{"class":533}," SortableTable",[488,1159,1160],{"class":498},"({ ",[488,1162,1163],{"class":569},"data",[488,1165,1166],{"class":498}," }",[488,1168,573],{"class":505},[488,1170,566],{"class":498},[488,1172,1163],{"class":569},[488,1174,573],{"class":505},[488,1176,579],{"class":533},[488,1178,1179],{"class":498},"[] }) {\n",[488,1181,1182,1184,1187,1189,1191,1194,1197,1199,1202,1204,1207,1210,1212,1215,1218],{"class":490,"line":502},[488,1183,833],{"class":505},[488,1185,1186],{"class":498}," [",[488,1188,732],{"class":618},[488,1190,409],{"class":498},[488,1192,1193],{"class":618},"setSort",[488,1195,1196],{"class":498},"] ",[488,1198,647],{"class":505},[488,1200,1201],{"class":533}," useState",[488,1203,593],{"class":498},[488,1205,1206],{"class":533},"SortState",[488,1208,1209],{"class":498},">({ key: ",[488,1211,659],{"class":494},[488,1213,1214],{"class":498},", dir: ",[488,1216,1217],{"class":494},"'ascending'",[488,1219,1220],{"class":498}," });\n",[488,1222,1223,1225,1227,1230,1232,1235,1237,1239,1241,1244,1247],{"class":490,"line":520},[488,1224,833],{"class":505},[488,1226,1186],{"class":498},[488,1228,1229],{"class":618},"message",[488,1231,409],{"class":498},[488,1233,1234],{"class":618},"setMessage",[488,1236,1196],{"class":498},[488,1238,647],{"class":505},[488,1240,1201],{"class":533},[488,1242,1243],{"class":498},"(",[488,1245,1246],{"class":494},"''",[488,1248,1249],{"class":498},");\n",[488,1251,1252],{"class":490,"line":527},[488,1253,524],{"emptyLinePlaceholder":523},[488,1255,1256,1258,1261,1263,1266,1269,1271],{"class":490,"line":556},[488,1257,833],{"class":505},[488,1259,1260],{"class":618}," sorted",[488,1262,537],{"class":505},[488,1264,1265],{"class":533}," useMemo",[488,1267,1268],{"class":498},"(() ",[488,1270,816],{"class":505},[488,1272,751],{"class":498},[488,1274,1275,1278,1281,1283,1285,1288],{"class":490,"line":607},[488,1276,1277],{"class":505},"    const",[488,1279,1280],{"class":618}," copy",[488,1282,537],{"class":505},[488,1284,1186],{"class":498},[488,1286,1287],{"class":505},"...",[488,1289,1290],{"class":498},"data];\n",[488,1292,1293,1296,1298,1301,1303,1305,1308,1310,1312],{"class":490,"line":612},[488,1294,1295],{"class":498},"    copy.",[488,1297,732],{"class":533},[488,1299,1300],{"class":498},"((",[488,1302,340],{"class":569},[488,1304,409],{"class":498},[488,1306,1307],{"class":569},"b",[488,1309,813],{"class":498},[488,1311,816],{"class":505},[488,1313,751],{"class":498},[488,1315,1316,1319,1322,1324],{"class":490,"line":653},[488,1317,1318],{"class":505},"      const",[488,1320,1321],{"class":618}," av",[488,1323,537],{"class":505},[488,1325,1326],{"class":498}," a[sort.key];\n",[488,1328,1329,1331,1334,1336],{"class":490,"line":671},[488,1330,1318],{"class":505},[488,1332,1333],{"class":618}," bv",[488,1335,537],{"class":505},[488,1337,1338],{"class":498}," b[sort.key];\n",[488,1340,1341,1343,1346,1348,1351,1353,1356,1358,1361,1364,1366,1368,1371,1373,1375,1378,1380,1383],{"class":490,"line":686},[488,1342,1318],{"class":505},[488,1344,1345],{"class":618}," cmp",[488,1347,537],{"class":505},[488,1349,1350],{"class":498}," av ",[488,1352,593],{"class":505},[488,1354,1355],{"class":498}," bv ",[488,1357,874],{"class":505},[488,1359,1360],{"class":505}," -",[488,1362,1363],{"class":618},"1",[488,1365,1028],{"class":505},[488,1367,1350],{"class":498},[488,1369,1370],{"class":505},">",[488,1372,1355],{"class":498},[488,1374,874],{"class":505},[488,1376,1377],{"class":618}," 1",[488,1379,1028],{"class":505},[488,1381,1382],{"class":618}," 0",[488,1384,499],{"class":498},[488,1386,1387,1390,1392,1394,1396,1398,1401,1403,1405],{"class":490,"line":701},[488,1388,1389],{"class":505},"      return",[488,1391,877],{"class":498},[488,1393,844],{"class":505},[488,1395,540],{"class":494},[488,1397,1022],{"class":505},[488,1399,1400],{"class":498}," cmp ",[488,1402,573],{"class":505},[488,1404,1360],{"class":505},[488,1406,1407],{"class":498},"cmp;\n",[488,1409,1410],{"class":490,"line":707},[488,1411,1412],{"class":498},"    });\n",[488,1414,1415,1418],{"class":490,"line":712},[488,1416,1417],{"class":505},"    return",[488,1419,1420],{"class":498}," copy;\n",[488,1422,1423],{"class":490,"line":724},[488,1424,1425],{"class":498},"  }, [data, sort]);\n",[488,1427,1428],{"class":490,"line":743},[488,1429,524],{"emptyLinePlaceholder":523},[488,1431,1432,1435,1438,1440,1442,1444,1446,1448],{"class":490,"line":754},[488,1433,1434],{"class":505},"  function",[488,1436,1437],{"class":533}," handleSort",[488,1439,1243],{"class":498},[488,1441,570],{"class":569},[488,1443,573],{"class":505},[488,1445,576],{"class":505},[488,1447,579],{"class":533},[488,1449,1450],{"class":498},") {\n",[488,1452,1453,1456,1458,1461,1463,1465],{"class":490,"line":782},[488,1454,1455],{"class":533},"    setSort",[488,1457,1300],{"class":498},[488,1459,1460],{"class":569},"prev",[488,1462,813],{"class":498},[488,1464,816],{"class":505},[488,1466,751],{"class":498},[488,1468,1469],{"class":490,"line":794},[488,1470,1471],{"class":853},"      \u002F\u002F Toggle direction if the same column, else start ascending\n",[488,1473,1474,1476,1479,1481,1483,1486,1489,1491],{"class":490,"line":824},[488,1475,1318],{"class":505},[488,1477,1478],{"class":618}," dir",[488,1480,573],{"class":505},[488,1482,561],{"class":533},[488,1484,1485],{"class":498},"[",[488,1487,1488],{"class":494},"'dir'",[488,1490,1196],{"class":498},[488,1492,1493],{"class":505},"=\n",[488,1495,1496,1499,1501,1504,1507,1510,1512,1514,1516,1518,1520,1522],{"class":490,"line":830},[488,1497,1498],{"class":498},"        prev.key ",[488,1500,844],{"class":505},[488,1502,1503],{"class":498}," key ",[488,1505,1506],{"class":505},"&&",[488,1508,1509],{"class":498}," prev.dir ",[488,1511,844],{"class":505},[488,1513,540],{"class":494},[488,1515,1022],{"class":505},[488,1517,546],{"class":494},[488,1519,1028],{"class":505},[488,1521,540],{"class":494},[488,1523,499],{"class":498},[488,1525,1526,1528,1531,1533,1535,1538,1541,1543,1546,1548,1550,1553,1555,1558,1561],{"class":490,"line":850},[488,1527,1318],{"class":505},[488,1529,1530],{"class":618}," label",[488,1532,537],{"class":505},[488,1534,619],{"class":618},[488,1536,1537],{"class":498},".",[488,1539,1540],{"class":533},"find",[488,1542,1300],{"class":498},[488,1544,1545],{"class":569},"c",[488,1547,813],{"class":498},[488,1549,816],{"class":505},[488,1551,1552],{"class":498}," c.key ",[488,1554,844],{"class":505},[488,1556,1557],{"class":498}," key)",[488,1559,1560],{"class":505},"!",[488,1562,1563],{"class":498},".label;\n",[488,1565,1566],{"class":490,"line":857},[488,1567,1568],{"class":853},"      \u002F\u002F Announce the new order — see next section\n",[488,1570,1571,1574,1576,1579,1581,1584,1586,1589],{"class":490,"line":886},[488,1572,1573],{"class":533},"      setMessage",[488,1575,1243],{"class":498},[488,1577,1578],{"class":494},"`Table sorted by ${",[488,1580,636],{"class":498},[488,1582,1583],{"class":494},"}, ${",[488,1585,585],{"class":498},[488,1587,1588],{"class":494},"}.`",[488,1590,1249],{"class":498},[488,1592,1593,1595],{"class":490,"line":891},[488,1594,1389],{"class":505},[488,1596,1597],{"class":498}," { key, dir };\n",[488,1599,1600],{"class":490,"line":900},[488,1601,1412],{"class":498},[488,1603,1604],{"class":490,"line":926},[488,1605,1606],{"class":498},"  }\n",[488,1608,1609],{"class":490,"line":938},[488,1610,524],{"emptyLinePlaceholder":523},[488,1612,1613,1615],{"class":490,"line":971},[488,1614,894],{"class":505},[488,1616,897],{"class":498},[488,1618,1619],{"class":490,"line":977},[488,1620,1621],{"class":498},"    \u003C>\n",[488,1623,1624,1626,1629],{"class":490,"line":988},[488,1625,941],{"class":498},[488,1627,1628],{"class":906},"table",[488,1630,1004],{"class":498},[488,1632,1633,1635,1638,1641,1643],{"class":490,"line":1007},[488,1634,991],{"class":498},[488,1636,1637],{"class":906},"caption",[488,1639,1640],{"class":498},">Q3 revenue by region (sortable)\u003C\u002F",[488,1642,1637],{"class":906},[488,1644,1004],{"class":498},[488,1646,1647,1649,1652],{"class":490,"line":1043},[488,1648,991],{"class":498},[488,1650,1651],{"class":906},"thead",[488,1653,1004],{"class":498},[488,1655,1656,1659,1662],{"class":490,"line":1053},[488,1657,1658],{"class":498},"          \u003C",[488,1660,1661],{"class":906},"tr",[488,1663,1004],{"class":498},[488,1665,1666,1669,1672,1674,1677,1679,1682,1684,1686],{"class":490,"line":1063},[488,1667,1668],{"class":498},"            {",[488,1670,1671],{"class":618},"COLUMNS",[488,1673,1537],{"class":498},[488,1675,1676],{"class":533},"map",[488,1678,1300],{"class":498},[488,1680,1681],{"class":569},"col",[488,1683,813],{"class":498},[488,1685,816],{"class":505},[488,1687,897],{"class":498},[488,1689,1690,1693,1696,1699,1701,1704,1707,1710,1713,1715,1718,1720,1722,1725,1727,1729],{"class":490,"line":1073},[488,1691,1692],{"class":498},"              \u003C",[488,1694,1695],{"class":618},"SortableHeader",[488,1697,1698],{"class":533}," key",[488,1700,647],{"class":505},[488,1702,1703],{"class":498},"{",[488,1705,1706],{"class":533},"String",[488,1708,1709],{"class":498},"(col.key)} ",[488,1711,1712],{"class":533},"column",[488,1714,647],{"class":505},[488,1716,1717],{"class":498},"{col} ",[488,1719,732],{"class":533},[488,1721,647],{"class":505},[488,1723,1724],{"class":498},"{sort} ",[488,1726,737],{"class":533},[488,1728,647],{"class":505},[488,1730,1731],{"class":498},"{handleSort} \u002F>\n",[488,1733,1734],{"class":490,"line":1079},[488,1735,1736],{"class":498},"            ))}\n",[488,1738,1740,1743,1745],{"class":490,"line":1739},37,[488,1741,1742],{"class":498},"          \u003C\u002F",[488,1744,1661],{"class":906},[488,1746,1004],{"class":498},[488,1748,1750,1752,1754],{"class":490,"line":1749},38,[488,1751,1046],{"class":498},[488,1753,1651],{"class":906},[488,1755,1004],{"class":498},[488,1757,1759,1761,1764],{"class":490,"line":1758},39,[488,1760,991],{"class":498},[488,1762,1763],{"class":906},"tbody",[488,1765,1004],{"class":498},[488,1767,1769,1772,1774,1776,1779,1781,1783],{"class":490,"line":1768},40,[488,1770,1771],{"class":498},"          {sorted.",[488,1773,1676],{"class":533},[488,1775,1300],{"class":498},[488,1777,1778],{"class":569},"row",[488,1780,813],{"class":498},[488,1782,816],{"class":505},[488,1784,897],{"class":498},[488,1786,1788,1791,1793,1795,1797],{"class":490,"line":1787},41,[488,1789,1790],{"class":498},"            \u003C",[488,1792,1661],{"class":906},[488,1794,1698],{"class":533},[488,1796,647],{"class":505},[488,1798,1799],{"class":498},"{row.id}>\n",[488,1801,1803,1805,1807,1809,1811,1814,1817,1819],{"class":490,"line":1802},42,[488,1804,1692],{"class":498},[488,1806,907],{"class":906},[488,1808,910],{"class":533},[488,1810,647],{"class":505},[488,1812,1813],{"class":494},"\"row\"",[488,1815,1816],{"class":498},">{row.region}\u003C\u002F",[488,1818,907],{"class":906},[488,1820,1004],{"class":498},[488,1822,1824,1826,1829,1832,1834],{"class":490,"line":1823},43,[488,1825,1692],{"class":498},[488,1827,1828],{"class":906},"td",[488,1830,1831],{"class":498},">{row.revenue}\u003C\u002F",[488,1833,1828],{"class":906},[488,1835,1004],{"class":498},[488,1837,1839,1841,1843,1846,1848],{"class":490,"line":1838},44,[488,1840,1692],{"class":498},[488,1842,1828],{"class":906},[488,1844,1845],{"class":498},">{row.growth}\u003C\u002F",[488,1847,1828],{"class":906},[488,1849,1004],{"class":498},[488,1851,1853,1856,1858],{"class":490,"line":1852},45,[488,1854,1855],{"class":498},"            \u003C\u002F",[488,1857,1661],{"class":906},[488,1859,1004],{"class":498},[488,1861,1863],{"class":490,"line":1862},46,[488,1864,1865],{"class":498},"          ))}\n",[488,1867,1869,1871,1873],{"class":490,"line":1868},47,[488,1870,1046],{"class":498},[488,1872,1763],{"class":906},[488,1874,1004],{"class":498},[488,1876,1878,1880,1882],{"class":490,"line":1877},48,[488,1879,1056],{"class":498},[488,1881,1628],{"class":906},[488,1883,1004],{"class":498},[488,1885,1887],{"class":490,"line":1886},49,[488,1888,524],{"emptyLinePlaceholder":523},[488,1890,1892,1894,1897],{"class":490,"line":1891},50,[488,1893,929],{"class":498},[488,1895,1896],{"class":853},"\u002F* Polite live region: announces sort changes without stealing focus *\u002F",[488,1898,935],{"class":498},[488,1900,1902,1904,1907,1910,1912,1915,1918,1920,1923,1926,1928,1931],{"class":490,"line":1901},51,[488,1903,941],{"class":498},[488,1905,1906],{"class":906},"div",[488,1908,1909],{"class":533}," role",[488,1911,647],{"class":505},[488,1913,1914],{"class":494},"\"status\"",[488,1916,1917],{"class":533}," aria-live",[488,1919,647],{"class":505},[488,1921,1922],{"class":494},"\"polite\"",[488,1924,1925],{"class":533}," className",[488,1927,647],{"class":505},[488,1929,1930],{"class":494},"\"sr-only\"",[488,1932,1004],{"class":498},[488,1934,1936],{"class":490,"line":1935},52,[488,1937,1938],{"class":498},"        {message}\n",[488,1940,1942,1944,1946],{"class":490,"line":1941},53,[488,1943,1056],{"class":498},[488,1945,1906],{"class":906},[488,1947,1004],{"class":498},[488,1949,1951],{"class":490,"line":1950},54,[488,1952,1953],{"class":498},"    \u003C\u002F>\n",[488,1955,1957],{"class":490,"line":1956},55,[488,1958,1076],{"class":498},[488,1960,1962],{"class":490,"line":1961},56,[488,1963,935],{"class":498},[389,1965],{},[392,1967,1969],{"id":1968},"announcing-the-sort-via-a-polite-live-region","Announcing the Sort via a Polite Live Region",[324,1971,1972,1973,1975,1976,1978,1979,1981],{},"Updating ",[328,1974,334],{}," is not enough on its own. A screen reader only re-reads ",[328,1977,334],{}," when the user navigates back to the header; immediately after pressing the button, focus stays on the header, so the user may hear nothing about the fact that potentially hundreds of rows just reordered. That silent reorder is exactly what ",[328,1980,381],{}," exists to prevent.",[324,1983,1984,1985,1988,1989,1992,1993,1996],{},"The fix is the polite live region in the component above. When the sort changes, we set a message like ",[328,1986,1987],{},"Table sorted by Revenue, descending."," into a ",[328,1990,1991],{},"role=\"status\""," region. Because it is ",[328,1994,1995],{},"polite",", it announces after the user's current speech finishes, without interrupting and without moving focus—the user stays on the header they just activated and hears confirmation of what happened.",[324,1998,1999,2000,2003,2004,2006],{},"Keep the message concise and consistent: column name, then direction. Avoid stuffing in the row count (\"...312 rows re-sorted\") unless your users have asked for it; verbose announcements on a frequently-used control quickly become noise. If you already centralize announcements with a shared ",[328,2001,2002],{},"useLiveAnnouncer"," hook from ",[340,2005,133],{"href":437},", call that instead of rendering a local region—just make sure the region exists in the DOM before the first announcement.",[389,2008],{},[392,2010,2012],{"id":2011},"keyboard-operability","Keyboard Operability",[324,2014,2015,2016,2018,2019,573],{},"Because the sort trigger is a native ",[328,2017,456],{},", keyboard support is mostly free—but verify each piece against ",[328,2020,370],{},[363,2022,2023,2029,2042],{},[366,2024,2025,2028],{},[359,2026,2027],{},"Tab \u002F Shift+Tab"," reaches every sortable header button in DOM order. There are no skipped or unreachable controls.",[366,2030,2031,2034,2035,2037,2038,2041],{},[359,2032,2033],{},"Enter and Space"," both activate the button and trigger the sort. A native ",[328,2036,456],{}," handles both keys for you; a ",[328,2039,2040],{},"\u003Cdiv role=\"button\">"," would force you to wire them manually—another reason to use the real element.",[366,2043,2044,2047],{},[359,2045,2046],{},"Focus stays on the activated header"," after sorting, so a keyboard user can press Enter again to toggle direction without hunting for the control. Do not move focus to the top of the table on sort; that would disorient users who were working within a specific column.",[324,2049,2050,2051,2054],{},"If a column is not sortable, render its header as a plain ",[328,2052,2053],{},"\u003Cth scope=\"col\">Label\u003C\u002Fth>"," with no button. Do not render a disabled button—an unsortable column simply has no control, which is clearer than a focusable-but-dead one.",[389,2056],{},[392,2058,2060],{"id":2059},"how-to-verify","How to Verify",[324,2062,2063],{},"Combine an automated pass with manual keyboard and screen reader checks—the automated tools confirm structure, but only manual testing confirms the experience.",[363,2065,2066,2086,2092,2106],{},[366,2067,2068,2071,2072,2074,2075,2077,2078,2081,2082,2085],{},[359,2069,2070],{},"Automated (axe-core \u002F jest-axe):"," Render the table and assert no violations. axe flags an invalid ",[328,2073,334],{}," value or a header with ",[328,2076,334],{}," that is not a ",[328,2079,2080],{},"columnheader",", but it cannot tell whether the sort ",[459,2083,2084],{},"announcement"," fires—that is a manual check.",[366,2087,2088,2091],{},[359,2089,2090],{},"Keyboard only:"," Unplug the mouse. Tab to a header, press Enter, then Space. Confirm the rows reorder, the visible arrow flips, and focus remains on the header. Tab through all headers and confirm none are skipped.",[366,2093,2094,2097,2098,409,2100,2103,2104,1537],{},[359,2095,2096],{},"Screen reader (NVDA + Firefox, VoiceOver + Safari):"," Navigate to a sortable header and confirm it is announced as a button. Activate it and confirm the polite region speaks \"Table sorted by ",[488,2099,1712],{},[488,2101,2102],{},"direction",".\" Navigate back to the header and confirm the screen reader reads the current sort state from ",[328,2105,334],{},[366,2107,2108,2111,2112,2114,2115,2117,2118,1537],{},[359,2109,2110],{},"State invariant:"," Inspect the DOM after several sorts and confirm exactly one ",[328,2113,450],{}," carries an ",[328,2116,334],{}," value other than ",[328,2119,1113],{},[2121,2122,2123],"blockquote",{},[324,2124,2125,2128,2129,2131,2132,2134,2135,2137],{},[359,2126,2127],{},"Testing Hook:"," A fast regression test is to assert in jest-axe that after a simulated click, exactly one header has ",[328,2130,334],{}," set to ",[328,2133,1107],{}," or ",[328,2136,1110],{}," and the live region's text content matches the expected message.",[389,2139],{},[392,2141,2143],{"id":2142},"common-a11y-mistakes","Common a11y Mistakes",[363,2145,2146,2160,2171,2180,2189,2200],{},[366,2147,2148,2159],{},[359,2149,2150,2151,2153,2154,2156,2157],{},"Using ",[328,2152,330],{}," instead of a ",[328,2155,456],{}," inside the ",[328,2158,450],{},", leaving the sort unreachable and unannounced for keyboard users.",[366,2161,2162,2170],{},[359,2163,2164,2165,2167,2168],{},"Putting ",[328,2166,334],{}," on the button rather than the ",[328,2169,450],{},", where assistive technology does not expect it as a column property.",[366,2172,2173,2179],{},[359,2174,2175,2176,2178],{},"Leaving ",[328,2177,334],{}," on multiple headers at once",", so users cannot tell which column actually drove the order.",[366,2181,2182,2188],{},[359,2183,2184,2185,2187],{},"Relying on ",[328,2186,334],{}," alone with no live region",", so the reorder is silent until the user happens to revisit the header.",[366,2190,2191,2194,2195,2197,2198,1537],{},[359,2192,2193],{},"Reading the visual arrow to screen readers"," by forgetting ",[328,2196,1087],{},", duplicating the direction already conveyed by ",[328,2199,334],{},[366,2201,2202,2205],{},[359,2203,2204],{},"Moving focus to the top of the table on sort",", disorienting keyboard users who were working in a specific column.",[389,2207],{},[392,2209,2211],{"id":2210},"frequently-asked-questions","Frequently Asked Questions",[324,2213,2214,2224,2225,2227,2228,2230,2231,351,2233,2235,2236,2238],{},[359,2215,2216,2217,2156,2219,2221,2222,874],{},"Why must the sort control be a ",[328,2218,456],{},[328,2220,450],{}," rather than a clickable ",[328,2223,450],{},"\nA bare ",[328,2226,330],{}," is not in the tab order and is not announced as interactive, so keyboard and screen reader users cannot operate or even discover it. A native ",[328,2229,456],{}," inside the header is focusable, handles Enter and Space automatically, and is announced as a button, satisfying ",[328,2232,370],{},[328,2234,376],{},", while the ",[328,2237,450],{}," keeps its role as a column header.",[324,2240,2241,2247,2248,2250,2251,2253],{},[359,2242,2243,2244,2246],{},"Should ",[328,2245,334],{}," go on the button or the table header cell?","\nIt belongs on the ",[328,2249,450],{},". ",[328,2252,334],{}," is a property of the column header, and assistive technology reads it when the user navigates to that header. Placing it on the inner button puts it on the wrong role and may not be reported as the column's sort state.",[324,2255,2256,2261,2263,2264,409,2266,2268,2269,2271],{},[359,2257,2258,2259,874],{},"Why do I need a live region if I already set ",[328,2260,334],{},[328,2262,334],{}," is only re-read when the user navigates back to the header. Right after activating the sort, focus stays on the header and the rows reorder silently. A polite live region announces \"Table sorted by ",[488,2265,1712],{},[488,2267,2102],{},"\" immediately, satisfying ",[328,2270,381],{},", so users learn the data changed without having to revisit the header.",[324,2273,2274,2277],{},[359,2275,2276],{},"Should I announce the number of rows that were re-sorted?","\nUsually no. Sorting is a frequent action, and a verbose announcement quickly becomes noise. Keep the message to the column name and direction. Add the row count only if your users specifically need it, and consider making it a user-configurable verbosity preference.",[389,2279],{},[392,2281,2283],{"id":2282},"related-guides","Related guides",[363,2285,2286,2290,2295],{},[366,2287,2288],{},[340,2289,109],{"href":342},[366,2291,2292],{},[340,2293,115],{"href":2294},"\u002Freact-nextjs-accessibility-patterns\u002Faccessible-data-tables-and-grids\u002Faccessible-pagination-for-react-data-tables\u002F",[366,2296,2297],{},[340,2298,133],{"href":437},[2300,2301,2302],"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":484,"searchDepth":502,"depth":502,"links":2304},[2305,2306,2308,2310,2311,2312,2313,2314,2315],{"id":394,"depth":502,"text":395},{"id":446,"depth":502,"text":2307},"A Sortable Header Is a Button Inside a \u003Cth>",{"id":1096,"depth":502,"text":2309},"Reflecting Direction With aria-sort",{"id":1968,"depth":502,"text":1969},{"id":2011,"depth":502,"text":2012},{"id":2059,"depth":502,"text":2060},{"id":2142,"depth":502,"text":2143},{"id":2210,"depth":502,"text":2211},{"id":2282,"depth":502,"text":2283},null,"Make column sorting accessible in React—use real buttons inside table headers, reflect direction with aria-sort, and announce the new order to screen reader users.","md",{},false,{"title":121,"description":2317},"jSBrlqyK8KDClTfrpIgvISXJeoak73cCVSou7TA2Fr8",[2324,2363,2364,2427],{"title":5,"path":6,"stem":7,"children":2325},[2326,2327,2330,2333,2339,2345,2354,2360],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2328},[2329],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2331},[2332],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2334},[2335,2336],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2337},[2338],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2340},[2341,2342],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2343},[2344],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2346},[2347,2348,2351],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2349},[2350],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2352},[2353],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69,"children":2355},[2356,2357],{"title":67,"path":68,"stem":69},{"title":73,"path":74,"stem":75,"children":2358},[2359],{"title":73,"path":74,"stem":75},{"title":79,"path":80,"stem":81,"children":2361},[2362],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87},{"title":89,"path":90,"stem":91,"children":2365},[2366,2367,2373,2385,2397,2400,2409,2421],{"title":94,"path":90,"stem":95},{"title":97,"path":98,"stem":99,"children":2368},[2369,2370],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2371},[2372],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2374},[2375,2376,2379,2382],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2377},[2378],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2380},[2381],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2383},[2384],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2386},[2387,2388,2391,2394],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2389},[2390],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2392},[2393],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2395},[2396],{"title":151,"path":152,"stem":153},{"title":157,"path":158,"stem":159,"children":2398},[2399],{"title":157,"path":158,"stem":159},{"title":163,"path":164,"stem":165,"children":2401},[2402,2403,2406],{"title":163,"path":164,"stem":165},{"title":169,"path":170,"stem":171,"children":2404},[2405],{"title":169,"path":170,"stem":171},{"title":175,"path":176,"stem":177,"children":2407},[2408],{"title":175,"path":176,"stem":177},{"title":181,"path":182,"stem":183,"children":2410},[2411,2412,2415,2418],{"title":181,"path":182,"stem":183},{"title":187,"path":188,"stem":189,"children":2413},[2414],{"title":187,"path":188,"stem":189},{"title":193,"path":194,"stem":195,"children":2416},[2417],{"title":193,"path":194,"stem":195},{"title":199,"path":200,"stem":201,"children":2419},[2420],{"title":199,"path":200,"stem":201},{"title":205,"path":206,"stem":207,"children":2422},[2423,2424],{"title":205,"path":206,"stem":207},{"title":211,"path":212,"stem":213,"children":2425},[2426],{"title":211,"path":212,"stem":213},{"title":217,"path":218,"stem":219,"children":2428},[2429,2430,2439,2448,2457,2466],{"title":222,"path":218,"stem":223},{"title":225,"path":226,"stem":227,"children":2431},[2432,2433,2436],{"title":225,"path":226,"stem":227},{"title":231,"path":232,"stem":233,"children":2434},[2435],{"title":231,"path":232,"stem":233},{"title":237,"path":238,"stem":239,"children":2437},[2438],{"title":237,"path":238,"stem":239},{"title":243,"path":244,"stem":245,"children":2440},[2441,2442,2445],{"title":243,"path":244,"stem":245},{"title":249,"path":250,"stem":251,"children":2443},[2444],{"title":249,"path":250,"stem":251},{"title":255,"path":256,"stem":257,"children":2446},[2447],{"title":255,"path":256,"stem":257},{"title":261,"path":262,"stem":263,"children":2449},[2450,2451,2454],{"title":261,"path":262,"stem":263},{"title":267,"path":268,"stem":269,"children":2452},[2453],{"title":267,"path":268,"stem":269},{"title":273,"path":274,"stem":275,"children":2455},[2456],{"title":273,"path":274,"stem":275},{"title":279,"path":280,"stem":281,"children":2458},[2459,2460,2463],{"title":279,"path":280,"stem":281},{"title":285,"path":286,"stem":287,"children":2461},[2462],{"title":285,"path":286,"stem":287},{"title":291,"path":292,"stem":293,"children":2464},[2465],{"title":291,"path":292,"stem":293},{"title":297,"path":298,"stem":299,"children":2467},[2468,2469,2472],{"title":297,"path":298,"stem":299},{"title":303,"path":304,"stem":305,"children":2470},[2471],{"title":303,"path":304,"stem":305},{"title":309,"path":310,"stem":311,"children":2473},[2474],{"title":309,"path":310,"stem":311},1781785524109]