Tablecraft
API Reference

useTableA11y

Standalone opt-in hook returning ARIA prop-getters implementing the WAI-ARIA Grid pattern, plus keyboard navigation.

Standalone opt-in hook. Returns prop-getter objects to spread onto your table elements, implementing the WAI-ARIA Grid pattern.

Signature

function useTableA11y<TData extends RowData>(
  table: TablecraftTable<TData>,
  options?: UseTableA11yOptions
): TableA11yReturn

Options

OptionTypeDescription
selectionEnabledbooleanSet to true when row selection is enabled on the table. Adds aria-selected to row props.
cellNavigationbooleanOpt in to cell-level (2D) keyboard navigation. Defaults to false, which keeps the row-level behaviour described below.

Return

PropertyTypeDescription
getTableProps() => { role: 'grid', 'aria-rowcount': number, 'aria-colcount': number }Spread onto the table element
getHeaderProps(headerId: string) => { role: 'columnheader', 'aria-sort': 'ascending' | 'descending' | 'none' }Spread onto each <th>
getRowProps(rowId: string) => { role: 'row', ref, 'aria-rowindex': number, 'aria-selected'?: boolean, 'aria-expanded'?: boolean, tabIndex: 0 | -1, onKeyDown }Spread onto each <tr>
getCellProps(columnIndex: number, rowId?: string) => { role: 'gridcell', 'aria-colindex': number, ref?, tabIndex?: 0 | -1, onKeyDown? }Spread onto each <td>. Pass rowId to enable cell navigation.
focusedRowIndexnumber | nullCurrent focused row index — useful for custom focus ring styling
focusedCell{ rowIndex, columnIndex } | nullFocused cell when cellNavigation is on, otherwise null. Row index is page-relative.

Usage

import { useTable, useTableA11y } from '@marvinackerman/tablecraft'

const { table } = useTable({ data, columns })
const a11y = useTableA11y(table, {
  selectionEnabled: true,  // adds aria-selected to row props
})

// Spread onto your elements:
<table {...a11y.getTableProps()} />
  // role="grid", aria-rowcount, aria-colcount

<th {...a11y.getHeaderProps(header.id)} />
  // role="columnheader", aria-sort="ascending"|"descending"|"none"

<tr {...a11y.getRowProps(row.id)} />
  // role="row", aria-rowindex, tabIndex, onKeyDown (ArrowUp/Down/Home/End)
  // aria-selected (when selectionEnabled), aria-expanded (when expandable)

<td {...a11y.getCellProps(cellIndex)} />
  // role="gridcell", aria-colindex

// Current focused row index (for custom focus ring styling):
a11y.focusedRowIndex  // number | null

Row navigation (the default — applied via onKeyDown on row props):

KeyAction
ArrowDownMove focus to next row
ArrowUpMove focus to previous row
HomeMove focus to first row
EndMove focus to last row
Enter / SpaceToggle row selection (when selectionEnabled)

Focus moves for real: spread the returned ref onto the element so the hook can move DOM focus as the roving tabindex changes. Mounting a grid never steals focus — only a keypress moves it.

Cell navigation

role="grid" promises two-dimensional keyboard navigation. Pass cellNavigation: true and give getCellProps the row id to provide it:

const a11y = useTableA11y(table, { cellNavigation: true })

<tr {...a11y.getRowProps(row.id)}>
  {row.getVisibleCells().map((cell, columnIndex) => (
    <td key={cell.id} {...a11y.getCellProps(columnIndex, row.id)}>
      {flexRender(cell.column.columnDef.cell, cell.getContext())}
    </td>
  ))}
</tr>

The roving tabindex moves from the row to the cell, so exactly one cell in the whole grid is tabbable at a time and the row handler goes inert (otherwise a keypress bubbling from cell to row would move twice).

KeyAction
ArrowRight / ArrowLeftMove one column, clamped at the edges
ArrowDown / ArrowUpMove one row, keeping the column
Home / EndFirst / last cell in the current row
Ctrl + Home / EndFirst / last cell in the grid
PageDown / PageUpMove ten rows, clamped
Enter / SpaceToggle selection of the focused cell's row (when selectionEnabled)

Row indices are page-relative because you can only focus a row that is actually rendered. aria-rowindex, by contrast, is the position within the full row set, so screen readers announce "row 12 of 50" correctly on page 2 rather than restarting at 1.

Standalone ARIA helpers

The following functions back useTableA11y internally and are also exported directly, in case you're computing ARIA attributes outside the hook (e.g. for a custom prop-getter or a non-hook rendering path):

function getAriaSortValue(headerId: string, sorting: SortingState): 'ascending' | 'descending' | 'none'
function getAriaRowCount<TData extends RowData>(table: TablecraftTable<TData>): number
function getAriaColCount<TData extends RowData>(table: TablecraftTable<TData>): number
function getAriaRowIndex<TData extends RowData>(rowId: string, rows: Row<TablecraftFeatures, TData>[]): number
FunctionDescription
getAriaSortValue(headerId, sorting)aria-sort value for a header cell. 'none' when the column is not currently sorted.
getAriaRowCount(table)Total number of filtered rows — used for aria-rowcount.
getAriaColCount(table)Number of visible leaf columns — used for aria-colcount.
getAriaRowIndex(rowId, rows)1-based row index of the given rowId within the provided rows array. Returns -1 if the row is not found (should not happen in normal usage). The hook passes it getPrePaginatedRowModel().rows, so the index covers the full sorted set rather than the current page.

On this page