Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

DisplayReact

Data Table

A native table of rows and columns that can sort, select rows and, in grid mode, move focus from cell to cell.

Rendered by React 19.3.1

Loading React…
import * as React from 'react'
import { DataTable, type DataTableSort } from '@hamur/react/data-table'

interface Member {
  value: string
  name: string
  role: string
  email: string
}

const members: Member[] = [
  { value: 'grace', name: 'Grace Hopper', role: 'Editor', email: 'grace@example.com' },
  { value: 'ada', name: 'Ada Lovelace', role: 'Admin', email: 'ada@example.com' },
  { value: 'katherine', name: 'Katherine Johnson', role: 'Viewer', email: 'katherine@example.com' },
  { value: 'alan', name: 'Alan Turing', role: 'Editor', email: 'alan@example.com' },
]

// The table holds the sort only; the app orders its rows.
function sorted(rows: Member[], sort: DataTableSort) {
  if (!sort) {
    return rows
  }
  const column = sort.column as 'name' | 'role'
  const order = sort.direction === 'ascending' ? 1 : -1
  return rows.toSorted((a, b) => a[column].localeCompare(b[column]) * order)
}

export default function DataTableExample() {
  const [sort, setSort] = React.useState<DataTableSort>(null)
  return (
    <DataTable.Root className="data-table" sort={sort} onSortChange={setSort}>
      <DataTable.Caption className="data-table-caption">
        Members
        <span className="sr-only"> (column headers with buttons are sortable)</span>
      </DataTable.Caption>
      <DataTable.Header>
        <DataTable.Row>
          <DataTable.ColumnHeader
            column="name"
            className="data-table-head"
          >
            <DataTable.SortTrigger className="data-table-sort">
              Name
              <DataTable.SortIndicator
                className="data-table-sort-indicator"
              />
            </DataTable.SortTrigger>
          </DataTable.ColumnHeader>
          <DataTable.ColumnHeader
            column="role"
            className="data-table-head"
          >
            <DataTable.SortTrigger className="data-table-sort">
              Role
              <DataTable.SortIndicator
                className="data-table-sort-indicator"
              />
            </DataTable.SortTrigger>
          </DataTable.ColumnHeader>
          <DataTable.ColumnHeader className="data-table-head">
            Email
          </DataTable.ColumnHeader>
        </DataTable.Row>
      </DataTable.Header>
      <DataTable.Body>
        {sorted(members, sort).map((member) => (
          <DataTable.Row
            key={member.value}
            value={member.value}
            className="data-table-row"
          >
            <DataTable.RowHeader className="data-table-cell">
              {member.name}
            </DataTable.RowHeader>
            <DataTable.Cell className="data-table-cell">
              {member.role}
            </DataTable.Cell>
            <DataTable.Cell className="data-table-cell">
              <a className="data-table-link" href={`mailto:${member.email}`}>
                {member.email}
              </a>
            </DataTable.Cell>
          </DataTable.Row>
        ))}
      </DataTable.Body>
    </DataTable.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<DataTable.Root>
  <DataTable.Caption />
  <DataTable.Header>
    <DataTable.Row>
      <DataTable.ColumnHeader>
        <DataTable.SortTrigger>
          <DataTable.SortIndicator />
        </DataTable.SortTrigger>
      </DataTable.ColumnHeader>
    </DataTable.Row>
  </DataTable.Header>
  <DataTable.Body>
      <DataTable.RowHeader />
      <DataTable.Cell />
  </DataTable.Body>
</DataTable.Root>

// Not in this example: DataTable.CheckboxIndicator, DataTable.Empty, DataTable.Footer, DataTable.RowCheckbox, DataTable.SelectAllCheckbox

Examples

Selection

selectable and multiple, a RowCheckbox per row and a SelectAllCheckbox; Alan Turing is disabled, and the selected values are listed below.

DataTableSelection.tsx
import * as React from 'react'
import { DataTable } from '@hamur/react/data-table'

const people = [
  { value: 'ada', name: 'Ada Lovelace', email: 'ada@example.com' },
  { value: 'grace', name: 'Grace Hopper', email: 'grace@example.com' },
  { value: 'alan', name: 'Alan Turing', email: 'alan@example.com', away: true },
  { value: 'katherine', name: 'Katherine Johnson', email: 'katherine@example.com' },
  { value: 'edsger', name: 'Edsger Dijkstra', email: 'edsger@example.com' },
]

function Check() {
  return (
    <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
      <path className="data-table-check-mark" d="M3 8.5l3.5 3.5 6.5-8" />
      <path className="data-table-check-dash" d="M4 8h8" />
    </svg>
  )
}

export default function DataTableSelectionExample() {
  const [value, setValue] = React.useState<string[]>(['ada'])
  return (
    <div className="stack">
      <DataTable.Root
        className="data-table"
        aria-label="Recipients"
        selectable
        multiple
        value={value}
        onValueChange={setValue}
      >
        <DataTable.Header>
          <DataTable.Row>
            <DataTable.ColumnHeader className="data-table-head data-table-check-column">
              <DataTable.SelectAllCheckbox
                className="data-table-checkbox"
              >
                <DataTable.CheckboxIndicator
                  className="data-table-check"
                >
                  <Check />
                </DataTable.CheckboxIndicator>
              </DataTable.SelectAllCheckbox>
            </DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Name</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Email</DataTable.ColumnHeader>
          </DataTable.Row>
        </DataTable.Header>
        <DataTable.Body>
          {people.map((person) => (
            <DataTable.Row
              key={person.value}
              value={person.value}
              disabled={person.away}
              className="data-table-row"
            >
              <DataTable.Cell className="data-table-cell data-table-check-column">
                <DataTable.RowCheckbox className="data-table-checkbox">
                  <DataTable.CheckboxIndicator className="data-table-check">
                    <Check />
                  </DataTable.CheckboxIndicator>
                </DataTable.RowCheckbox>
              </DataTable.Cell>
              <DataTable.RowHeader className="data-table-cell">
                {person.name}
              </DataTable.RowHeader>
              <DataTable.Cell className="data-table-cell">
                {person.away ? 'Away until Monday' : person.email}
              </DataTable.Cell>
            </DataTable.Row>
          ))}
        </DataTable.Body>
      </DataTable.Root>
      <p className="muted">Selected: {value.join(', ') || 'none'}</p>
    </div>
  )
}

Grid

grid: tasks with an owner button and Edit and Delete in their cells; Enter on a row header runs the row's onAction, and Empty shows when all are deleted.

DataTableGrid.tsx
import * as React from 'react'
import { DataTable } from '@hamur/react/data-table'

interface Task {
  value: string
  title: string
  status: string
  owner: string
}

const initial: Task[] = [
  { value: 'audit', title: 'Audit the forms', status: 'In progress', owner: 'Ada' },
  { value: 'icons', title: 'Redraw the icons', status: 'Waiting', owner: 'Grace' },
  { value: 'docs', title: 'Write the docs', status: 'Done', owner: 'Alan' },
  { value: 'release', title: 'Cut the release', status: 'Waiting', owner: 'Katherine' },
]

export default function DataTableGridExample() {
  const [tasks, setTasks] = React.useState(initial)
  const [last, setLast] = React.useState('none')
  return (
    <div className="stack">
      <DataTable.Root className="data-table" aria-label="Tasks" grid>
        <DataTable.Header>
          <DataTable.Row>
            <DataTable.ColumnHeader className="data-table-head">Task</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Status</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Owner</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Actions</DataTable.ColumnHeader>
          </DataTable.Row>
        </DataTable.Header>
        <DataTable.Body>
          {tasks.map((task) => (
            <DataTable.Row
              key={task.value}
              value={task.value}
              className="data-table-row"
              onAction={() => setLast(`Opened ${task.title}`)}
            >
              <DataTable.RowHeader className="data-table-cell">
                {task.title}
              </DataTable.RowHeader>
              <DataTable.Cell className="data-table-cell">
                {task.status}
              </DataTable.Cell>
              <DataTable.Cell className="data-table-cell">
                <button
                  type="button"
                  className="data-table-action"
                  onClick={() => setLast(`Messaged ${task.owner}`)}
                >
                  {task.owner}
                </button>
              </DataTable.Cell>
              <DataTable.Cell className="data-table-cell">
                <span className="data-table-actions">
                  <button
                    type="button"
                    className="data-table-action"
                    aria-label={`Edit ${task.title}`}
                    onClick={() => setLast(`Edited ${task.title}`)}
                  >
                    Edit
                  </button>
                  <button
                    type="button"
                    className="data-table-action"
                    aria-label={`Delete ${task.title}`}
                    onClick={() => {
                      setTasks((previous) => previous.filter((t) => t.value !== task.value))
                      setLast(`Deleted ${task.title}`)
                    }}
                  >
                    Delete
                  </button>
                </span>
              </DataTable.Cell>
            </DataTable.Row>
          ))}
          <DataTable.Empty className="data-table-row">
            <DataTable.Cell className="data-table-cell data-table-empty" colSpan={4}>
              No tasks
            </DataTable.Cell>
          </DataTable.Empty>
        </DataTable.Body>
      </DataTable.Root>
      <p className="muted">Last action: {last}</p>
    </div>
  )
}

Pagination

23 invoices, five a page, with Pagination below; the app slices its rows by the page and the tab stop moves to the new page's first row.

DataTablePagination.tsx
import * as React from 'react'
import { DataTable } from '@hamur/react/data-table'
import { Pagination } from '@hamur/react/pagination'

const PAGE_SIZE = 5
const invoices = Array.from({ length: 23 }, (_, i) => ({
  value: `INV-${1001 + i}`,
  customer: ['Ada', 'Grace', 'Alan', 'Katherine', 'Edsger'][i % 5],
  amount: `$${(120 + i * 15).toFixed(2)}`,
}))

export default function DataTablePaginationExample() {
  const [page, setPage] = React.useState(1)
  const rows = invoices.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
  return (
    <div className="stack">
      <DataTable.Root className="data-table" aria-label="Invoices" grid>
        <DataTable.Header>
          <DataTable.Row>
            <DataTable.ColumnHeader className="data-table-head">Invoice</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Customer</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head data-table-number">
              Amount
            </DataTable.ColumnHeader>
          </DataTable.Row>
        </DataTable.Header>
        <DataTable.Body>
          {rows.map((invoice) => (
            <DataTable.Row key={invoice.value} value={invoice.value} className="data-table-row">
              <DataTable.RowHeader className="data-table-cell">
                {invoice.value}
              </DataTable.RowHeader>
              <DataTable.Cell className="data-table-cell">
                {invoice.customer}
              </DataTable.Cell>
              <DataTable.Cell className="data-table-cell data-table-number">
                {invoice.amount}
              </DataTable.Cell>
            </DataTable.Row>
          ))}
        </DataTable.Body>
      </DataTable.Root>
      <Pagination.Root
        className="pagination pagination-compact"
        aria-label="Invoice pages"
        itemCount={invoices.length}
        pageSize={PAGE_SIZE}
        value={page}
        onValueChange={setPage}
      >
        <Pagination.Previous className="pagination-control">‹</Pagination.Previous>
        <Pagination.Value className="pagination-value" />
        <Pagination.Next className="pagination-control">›</Pagination.Next>
      </Pagination.Root>
    </div>
  )
}

Virtualized

1,000 orders as rows, virtualized, with fewer than 30 rows mounted; Control+End and the page keys reach rows that are not mounted.

DataTableVirtualized.tsx
import * as React from 'react'
import { DataTable } from '@hamur/react/data-table'

// Rows of a fixed height, windowed by hand: only the rows near the scroll position render.
const ROW = 36
const HEAD = 36
const VIEWPORT = 288
const OVERSCAN = 10
const COUNT = 1000

// Every row's value, mounted or not: Ctrl+End, the page keys and select-all work on it.
const orders = Array.from({ length: COUNT }, (_, index) => index + 1)
const label = (order: number) => `Order ${String(order).padStart(4, '0')}`
const customers = ['Ada', 'Grace', 'Alan', 'Katherine', 'Edsger', 'Barbara', 'John']
const total = (order: number) => `$${((order * 37) % 900) + 20}.00`

export default function DataTableVirtualizedExample() {
  const scrollerRef = React.useRef<HTMLDivElement | null>(null)
  const [scrollTop, setScrollTop] = React.useState(0)
  const first = Math.max(0, Math.floor(scrollTop / ROW) - OVERSCAN)
  const last = Math.min(COUNT, Math.ceil((scrollTop + VIEWPORT) / ROW) + OVERSCAN)
  // The table asks for the row at `index` to be scrolled into view, then focuses it once it mounts.
  function scrollToIndex(index: number) {
    const scroller = scrollerRef.current
    if (!scroller) {
      return
    }
    const top = index * ROW
    if (top < scroller.scrollTop) {
      scroller.scrollTop = top
    } else if (top + ROW + HEAD > scroller.scrollTop + scroller.clientHeight) {
      scroller.scrollTop = top + ROW + HEAD - scroller.clientHeight
    }
    setScrollTop(scroller.scrollTop)
  }

  return (
    <div
      ref={scrollerRef}
      className="data-table-scroller"
      onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
    >
      <DataTable.Root
        className="data-table data-table-virtual"
        aria-label="1,000 orders"
        grid
        rows={orders}
        virtualized
        onRowHighlighted={(_row: number, { index }: { index: number }) => scrollToIndex(index)}
      >
        <DataTable.Header className="data-table-virtual-head">
          <DataTable.Row className="data-table-virtual-row">
            <DataTable.ColumnHeader className="data-table-head">Order</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head">Customer</DataTable.ColumnHeader>
            <DataTable.ColumnHeader className="data-table-head data-table-number">
              Total
            </DataTable.ColumnHeader>
          </DataTable.Row>
        </DataTable.Header>
        <DataTable.Body className="data-table-virtual-body" style={{ height: COUNT * ROW }}>
          {orders.slice(first, last).map((order, offset) => (
            <DataTable.Row
              key={order}
              value={order}
              index={first + offset}
              className="data-table-row data-table-virtual-row"
              style={{ transform: `translateY(${(first + offset) * ROW}px)` }}
            >
              <DataTable.RowHeader className="data-table-cell">
                {label(order)}
              </DataTable.RowHeader>
              <DataTable.Cell className="data-table-cell">
                {customers[order % customers.length]}
              </DataTable.Cell>
              <DataTable.Cell className="data-table-cell data-table-number">
                {total(order)}
              </DataTable.Cell>
            </DataTable.Row>
          ))}
        </DataTable.Body>
      </DataTable.Root>
    </div>
  )
}

API reference

40 props across 15 parts, in React syntax. className, style, render are listed once, under Shared props.

BodyDataTable.Body

The body row group: the data rows, and DataTable.Empty while there are none. Renders a <tbody>.

CaptionDataTable.Caption

The title of the table: it names the table unless aria-label or aria-labelledby is passed to the root. Place it as the first child of DataTable.Root. Renders a <caption>.

CellDataTable.Cell

A data cell of a row; a gridcell in grid mode. Renders a <td>.

Cell props
PropTypeDefault
columnIndexThe 0-based position of the cell's column among all columns, rendered or not.number—
Cell data attributes
Data attributeDescription
data-selectedPresent when the cell's row is selected.
data-disabledPresent when the cell's row is disabled.
data-highlightedPresent in grid mode when focus is on the cell or inside it.

CheckboxIndicatorDataTable.CheckboxIndicator

Shows the tick of a DataTable.RowCheckbox or DataTable.SelectAllCheckbox, rendered while it is checked or mixed. Renders a <span>.

CheckboxIndicator props
PropTypeDefault
keepMountedWhether to keep the element in the DOM when the checkbox is not checked.booleanfalse

ColumnHeaderDataTable.ColumnHeader

The header of a column. It carries aria-sort while the root's sort is on its column, whatever set the sort. Renders a <th>.

ColumnHeader props
PropTypeDefault
columnThe column's key: the header carries aria-sort while the root's sort.column equals it,string—
labelThe column's name in the sort announcement.string—
columnIndexThe 0-based position of the column among all columns, rendered or not.number—
ColumnHeader data attributes
Data attributeDescription
data-sortablePresent when the header holds a DataTable.SortTrigger.
data-sort-directionThe direction of the column's sort, while it is sorted.
data-highlightedPresent in grid mode when focus is on the header or inside it.

EmptyDataTable.Empty

The row shown while the body has no rows; put a DataTable.Cell with colSpan inside. It is not a grid row: arrows skip it, and its controls are reached by Tab. Renders nothing otherwise. Place it inside DataTable.Body. Renders a <tr>.

The footer row group: summary rows. Renders a <tfoot>.

HeaderDataTable.Header

The header row group: the rows of column headers. Renders a <thead>.

RootDataTable.Root

Groups all parts of the data table: a native table, or with grid an interactive grid with cell focus., followed by the status region. Renders a <table>.

Root props
PropTypeDefault
gridWhether the table is an interactive grid: role="grid", cell focus, the grid keys and onebooleanfalse
sortThe current sort: a column and its direction, or null.DataTableSort—
defaultSortThe uncontrolled sort when the table is initially rendered.DataTableSort—
onSortChangeEvent handler called when a DataTable.SortTrigger is pressed.((sort: DataTableSort, eventDetails: { reason: "trigger-press"; event: MouseEvent | PointerEvent | KeyboardEvent | TouchEvent; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element | undefined; }) => void)—
allowUnsortedWhether the sort cycle returns to unsorted after the second direction.booleantrue
valueThe selected rows.DataTableInputValue<Value, Multiple> | null—
defaultValueThe uncontrolled selected rows when the table is initially rendered.DataTableInputValue<Value, Multiple> | null—
onValueChangeEvent handler called when the selection changes, once per gesture.((value: DataTableOutputValue<Value, Multiple>, eventDetails: DataTableRootChangeEventDetails) => void)—
selectableWhether rows can be selected at all.booleanfalse
multipleWhether several rows can be selected.Multiplefalse
disallowEmptySelectionWhether the gesture that would clear the last selected row is refused.booleanfalse
selectOnPressWhether a press on a selectable row outside its controls selects it.booleanfalse
isRowEqualToValueCustom comparison logic used to determine if a row value matches a selected value.((row: Value, value: Value) => boolean)—
disabledWhether the component should ignore user interaction.booleanfalse
rowsEvery body row's value in order, rendered or not: for virtualized, and for select-all andreadonly Value[]—
virtualizedWhether the app renders only some body rows (a virtualizer), each with its index.booleanfalse
isRowDisabledWhether a value of rows is disabled, for rows that are not rendered.((row: Value) => boolean)—
onRowHighlightedVirtualized grid only: called when a key targets a row that is not rendered.((row: Value, eventDetails: DataTableHighlightDetails) => void)—
columnCountThe number of columns the table has, rendered or not.number—
labelsThe texts of the status region and the checkboxes.Partial<DataTableLabels>—
Root data attributes
Data attributeDescription
data-gridPresent in grid mode.
data-selectablePresent when rows can be selected.
data-multiplePresent when several rows can be selected.
data-disabledPresent when the table is disabled.
data-emptyPresent when the body has no rows.
data-sortedPresent when a sort is set.

RowDataTable.Row

A row of the table, in its header, body or footer. Renders a <tr>.

Row props
PropTypeDefault
childrenReact.ReactNode—
valueA unique value that identifies this row.any—
labelThe row's name in the announcements, when it has no DataTable.RowHeader.string—
disabledWhether the component should ignore user interaction.booleanfalse
selectableWhether the row can be selected, in a table that is selectable.booleantrue
onActionGrid mode only: called when the row is activated (Enter on a cell without controls, or a((eventDetails: DataTableActionDetails) => void)—
indexVirtualized only: the position of the row in rows.number—
Row data attributes
Data attributeDescription
data-selectedPresent when the row is selected.
data-disabledPresent when the row is disabled.
data-highlightedPresent in grid mode when focus is in one of the row's cells.

RowCheckboxDataTable.RowCheckbox

The checkbox that selects its row. Named "Select" plus the row's DataTable.RowHeader. A press or Space toggles the row; with Shift it selects the range from the last toggled row. Renders a <span>.

RowCheckbox props
PropTypeDefault
disabledWhether the component should ignore user interaction.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse

RowHeaderDataTable.RowHeader

The cell that names its row: the row's checkbox, its grid row and the announcements read it. Renders a <th>.

RowHeader props
PropTypeDefault
columnIndexThe 0-based position of the cell's column among all columns, rendered or not.number—

SelectAllCheckboxDataTable.SelectAllCheckbox

The checkbox that selects every row, or clears them when all are selected; mixed while some are. Disabled rows keep their state. Needs a multiple table. Renders a <span>.

SelectAllCheckbox props
PropTypeDefault
disabledWhether the component should ignore user interaction.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse

SortIndicatorDataTable.SortIndicator

Shows the direction of its column's sort; hidden from assistive technology, which reads aria-sort. Always rendered. Place it inside a DataTable.ColumnHeader. Renders a <span>.

SortTriggerDataTable.SortTrigger

The button that sorts its column. Its content is its name. Place it inside a DataTable.ColumnHeader that has a column. Renders a <button>.

SortTrigger props
PropTypeDefault
firstDirectionThe direction a press sorts an unsorted column in.DataTableSortDirection'ascending'
disabledWhether the component should ignore user interaction.booleanfalse
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
EnterSpaceSorts by the column: its first direction, then the other, then noneSortTrigger
SpaceIn table mode, toggles the focused row checkboxRowCheckbox
ShiftSpaceOn a row checkbox, selects the range from the anchorRowCheckbox
TabIn grid mode, enters the table on its one tab stopRoot
ArrowRightArrowLeftIn grid mode, moves to the next or previous cell and stops at the edgesRoot
ArrowDownArrowUpMoves to the cell below or above in the same column, header and footer includedRoot
HomeEndMoves to the first or last cell of the rowRoot
ControlHomeControlEndMoves to the first or last cell of the table, mounted or notRoot
PageDownPageUpMoves ten rows in the same column and stops at the endsRoot
EnterF2On a cell with several controls, moves to its first one; F2 returnsCell
EscapeFrom a control, returns to its cellCell
EnterOn a cell without controls, runs the row's onActionRow
SpaceIn grid mode, toggles the focused rowRow
ShiftArrowDownShiftArrowUpWith multiple, extends the selection from the anchorRoot
ControlAMetaAWith multiple in grid mode, selects every enabled row, or clears them when all are selectedRoot

Accessibility

What the parts set
PartSetsSo that
Rootthe native table, role="grid" with grid, aria-multiselectable with multiple in grid mode, aria-rowcount when virtualizeda table is read as a table, and a grid as one widget; a development build warns without a name
Captionthe native caption, which names the table; an aria-label on Root wins over itthe table is announced by its name when a reader enters it
ColumnHeadercolumnheader, aria-sort on the sorted header only, never "none"the sort is read from the header, not from the indicator
RowHeaderrowheader; in grid mode it names its row (aria-labelledby)moving down a column reads which row each cell belongs to
Rowaria-selected on selectable rows and aria-disabled on disabled ones in grid mode only; aria-rowindex when virtualizeda table-mode row says nothing it cannot act on; the checkbox carries the state there
RowCheckboxrole="checkbox", named "Select" plus its row header; aria-disabled on a disabled rowten checkboxes are not all read as "Select"
SelectAllCheckboxrole="checkbox", named "Select all", aria-checked="mixed" while some rows are selectedthe header checkbox tells whether the page is all, some or none selected
SortIndicatoraria-hiddenthe arrow is for sight; aria-sort says it
Rootone polite role="status" region next to the table, always mountedsorts, ranges and select-all are announced, since readers do not announce aria-sort or aria-selected changes

Focus order

  1. In table mode the table is not a stop: Tab walks its sort buttons, checkboxes, links and buttons in reading order.
  2. In grid mode the table is one Tab stop. Inside, the arrow keys, Home, End, Control+Home, Control+End and the page keys move between cells; a cell whose only control is a button, link or checkbox focuses that control.
  3. A cell with several controls is entered with Enter or F2; Tab walks its controls and then leaves the table, and Escape or F2 returns to the cell.