Skip to content

Accessible React data grid — keyboard, contrast

See it working: arrow through a Mantine table and read the live-region transcript — Tab in, arrow between cells, and every announcement appears as text. The same page exists for MUI, Chakra, antd, Radix, Base UI, shadcn and Tailwind. Keyboard walk is documented in cell navigation.

An accessible data table is one a person can use without a mouse, and one a screen reader can describe. AdaptTable ships that way. There is no accessible prop to turn on.

Related: Keyboard & cell navigation · i18n & RTL · Browser and server-built exports · FAQ

Every table you render already:

  • uses a real <table> with header and body cells
  • names every control it draws (Filters, checkboxes, close, Done — not icon-only)
  • marks sortable headers with aria-sort
  • states the real dataset size when only part of it is in the DOM — a virtualized or paged table carries aria-rowcount with each row’s absolute aria-rowindex, a table whose columns are windowed carries aria-colcount with an absolute aria-colindex on every body and header cell, and the mobile card list carries aria-setsize with each card’s aria-posinset, so a screen reader says “row 40,001 of 50,000” instead of counting the handful of rows it can reach
  • says what changed when the rows change — sorting, filtering, paging and page size announce politely through one live region, naming the new order or the new count in the same words the footer shows (“Sorted by Name, ascending”, “Page 2 of 4. Showing 26–50 of 87”), because a control that rewrites the table silently gives a screen-reader user no way to tell it worked
  • honours prefers-reduced-motion when rows animate in
  • honours forced-colors: active (Windows High Contrast) and prefers-contrast: more: focus, selection, dirty cells, validation errors and find-match highlights each keep an outline, not a fill or a box-shadow, so nothing is signaled by colour alone. Borders, pinned-column edges and overlays use system colors (Canvas, CanvasText, Highlight)

Every adapter is audited with axe in CI, on desktop and mobile card layouts.

On the accessibility demo:

  1. Press Tab until a cell shows a focus ring.
  2. Press the arrow keys. The ring moves cell to cell.
  3. Open a cell with Enter or a double-click; Escape cancels.

If Tab never enters the table or arrows do nothing, that page is failing.

Optional row reordering is keyboard-complete: Space lifts, arrows choose a visible target, Space drops, and Escape cancels. Group and tree moves also have a Move to group… / Move under… menu, so re-parenting never depends on drag precision. Confirm/cancel restores focus to that menu trigger, and a live region announces successful moves, policy rejections, sort conflicts, and cycle guards.

Server-built exports are keyboard-complete too. Their kit-native progress surface exposes Cancel while busy, Retry after failure, and a real download link after { url } settles. A polite region announces start, each reported progress value, completion, failure, and cancellation; no progress report stays indeterminate without repeatedly announcing a fake percentage.

The optional spreadsheet grid — one Tab stop, arrow keys through every cell, role="grid" — is a separate feature. See keyboard & cell navigation. Without the cellNavigation() feature the grid extras are absent; the default table above still stands.

Windows High Contrast (and other forced-colors: active modes) strips the fills and box-shadows a kit painted. AdaptTable does not ask each kit to re-theme that itself. One stylesheet — ForcedColorsStyle, mounted from every adapter — switches those marks to system colors:

Mark Affordances that survive
Focus outline in Highlight, never a box-shadow
Selection solid outline (also in the cell style, so it is not CSS-only)
Find match dashed outline; the current hit is solid
Dirty cell dotted outline — data-dirty is never colour-only
Validation double outline on aria-invalid
Pinned edge CanvasText border on the pin side
Overlays find bar, command palette, saved views, side panel use Canvas / CanvasText borders

prefers-contrast: more thickens the focus outline to 3px. Kits that expose no contrast hook of their own still pick this up: the query is global.

The antd adapter keeps its sticky header. The role="grid" lives on the wrapper around both of antd’s tables so a screen reader walking a cell still finds that cell’s columnheader in the same grid.

  • Works in all eight adapters. The demo is the same walk on each kit.
  • Labels you pass through labels are the accessible names, including in Arabic and the other bundled locales.