Customization

Accessibility

A div-based ARIA grid with one tab stop, full keyboard navigation, a live region that speaks the grid's own language, and a layout that mirrors under RTL.

One Tab Stop

Click a cell in the grid below, then walk it with the arrow keys. Press Tab and you leave the grid in one press rather than one per cell: the cells carry a roving tabindex, and every control inside a cell answers through the cell that holds it.

That is also why the selection checkbox is not tabbable, and why Space on the cell toggles it instead.

Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
Mai Nguyen
Support
$83,226.00
Bruno Novak
Support
$71,507.00
8 rows
// One tab stop. The cells carry a roving tabindex: the active one is 0,
// every other is -1, so Tab leaves the grid rather than walking it.
//
// A control inside a cell answers through the cell rather than taking a
// stop of its own. That is why the selection checkbox is not tabbable and
// Space on the cell toggles it instead: a hundred-row grid used to cost a
// hundred presses to step over.

// Where the focus is, and how to move it
grid.focus.active;                         // { row, col, section? }
grid.focus.focusCell({ row: 0, col: 2 });
grid.focus.moveBy(1, 0);

// A negative row is the header: the body starts at 0, so a focus sitting
// above it is the header row rather than a row of data.
grid.focus.active.row < 0;

Navigation

KeysMoves
Arrow keysMove one cell, header row included
Arrow down from a headerThe filter row when one is drawn, then the first body row. It is a line of the grid rather than a strip above it, which is what makes it reachable at all
Arrow up from a leaf headerInto the header groups above it, level by level. A column with no group above it has nowhere to go up to
Arrow left or right on a groupSteps between the groups of that level rather than between columns
Home / EndFirst or last cell of the row
Ctrl + Home / EndFirst or last cell of the grid
Page up / Page downOne viewport of rows at a time
TabLeaves the grid, because the whole grid is one tab stop

Actions

Every binding a registered feature contributes. A feature that is not registered contributes nothing, so its keys stay free.

KeysDoes
SpaceToggles selection on the focused row
Enter or Space on a groupFolds or unfolds the header group under the caret, and the announcer says which it did
Shift + SpaceExtends the selection from the anchor row
Ctrl or Cmd + ASelects every selectable row
Ctrl or Cmd + CCopies the selection as TSV
Ctrl or Cmd + VPastes tab-separated text from the focused cell. Bound to the paste event rather than to the keystroke, so a right-click paste works and no clipboard permission is asked for
Enter or F2Opens the editor, or sorts when the header has focus
A printable keyOpens a text, number or textarea editor seeded with the character that was typed
Ctrl or Cmd + EnterCommits an editor that claims Enter for itself
EscapeCloses what the editor opened, a menu or a calendar, and then the editor itself
Ctrl + Z, Ctrl + Shift + Z, Ctrl + YUndo and redo an edit; both redo forms are bound
Shift + EnterAdds the focused header column to the sort
Shift + Arrow left or rightResizes the focused column
Alt + Arrow left or rightMoves the focused column
Alt + Arrow up or downMoves the focused row, or opens the column menu on a header
Arrow right or left on a nested rowExpands the row, collapses it, and from a collapsed child steps to its parent. Enter toggles the same row

The ARIA Shape

Positions are absolute rather than relative to the rendered window, which is what lets a virtualized grid tell the truth: row 1,400 of 2,000 says exactly that, even though only twenty rows exist in the DOM.

The grid below holds 2,000 rows and renders a dozen or so. Click a cell, sort a column, tick a row, then read the attributes off the DOM: the count never shrinks to the window, and the row index says where the row sits in the whole set. The count reads 2,001 because ARIA counts the header row too. The last badge is the exception that proves the point: it counts rows in the DOM, so sorting a selected row out of the window takes it back to zero while the grid still holds the selection.

role=- aria-rowcount=- 0 rows in the DOM
aria-rowindex=- aria-colindex=- aria-sort=- aria-selected on - rows
Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
Mai Nguyen
Support
$83,226.00
Bruno Novak
Support
$71,507.00
Grace Dubois
Support
$94,212.00
Tuan Ivanov
Design
$105,520.00
Bruno Andersen
Growth
$62,389.00
Bruno Yilmaz
Core
$139,212.00
Grace Costa
Platform
$96,734.00
Niko Muller
Core
$76,769.00
Olga Yilmaz
Platform
$66,068.00
Dara Okafor
Platform
$87,888.00
Quyen Silva
Design
$121,817.00
Grace Haddad
Support
$137,633.00
Keiko Novak
Core
$125,071.00
Tuan Dubois
Growth
$73,645.00
2,000 rows
<!-- A div-based ARIA grid, or treegrid once rows nest. Every row and cell
     carries its position, so a screen reader can say where it is even
     though only a window of rows exists in the DOM. -->
<div role="grid" aria-rowcount="2000" aria-colcount="5">
  <div role="rowgroup">
    <div role="row" aria-rowindex="1">
      <div role="columnheader" aria-colindex="1" aria-sort="ascending" tabindex="0">Name</div>
    </div>
  </div>
  <div role="rowgroup">
    <div role="row" aria-rowindex="42" data-dg-row-id="41">
      <div role="gridcell" aria-colindex="1" data-dg-cell="40:0" tabindex="-1">Ada</div>
    </div>
  </div>
</div>

ARIA Attributes

AttributeWhere and why
role="grid" / "treegrid"On the viewport. It reads grid.expansion.enabled, which an app sets when its rows nest: aria-level on the rows is not enough on its own, since a level can describe depth in a flat list
aria-rowcount / aria-colcountThe whole set, not the rendered window. The rows above the body are counted too: the header levels, the leaf header row, and the filter row when one is drawn
aria-rowindex / aria-colindexAbsolute position, so a virtualized row still says where it is
aria-sortOn a sorted column header
aria-selectedOn a selected row. A feature can put it on a cell through its decoration
aria-colspan / aria-rowspanOn a merged cell, which is the single tab stop for its block
aria-expanded / aria-levelOn rows that nest
aria-invalidOn an editor whose value failed validation
aria-live="polite"The announcer region, outside the grid

The Announcer

Sorting, filtering, paging, selection, column changes, row moves and a blocked edit all go through a polite live region. Every string belongs to the locale pack, so it speaks the language the grid is in, and any of them can be replaced.

The region is invisible, so the panel below mirrors it. Sort a column, type in the quick filter, tick a row or move a column, and read what a screen reader would have heard.

aria-live="polite", last five

Nothing said yet. Sort a column to start.

Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
Quyen Tanaka
Growth
$72,011.00
Mai Nguyen
Support
$83,226.00
Bruno Novak
Support
$71,507.00
Grace Dubois
Support
$94,212.00
Tuan Ivanov
Design
$105,520.00
Bruno Andersen
Growth
$62,389.00
Bruno Yilmaz
Core
$139,212.00
12 rows
// A polite live region announces what a sighted user sees change.
// Every string is part of the locale pack, so it speaks the grid's
// language, and any of them can be replaced.
createDataGrid<Person>({
  columns, data, getRowId,
  announcer: {
    sorted: (column, direction) => `${column}, ${direction}ending`,
    filtered: (count) => `${count} rows left`,
    selected: (count) => `${count} selected`,
    rowMoved: (position) => `moved to ${position}`
  }
});

// The full set also covers sortCleared, page, columnResized, columnMoved,
// columnPinned, columnVisibility, copied, rowExpanded, rowPinned and
// editInvalid.

What It Says

Every string the announcer speaks. Each one belongs to the locale pack, so it is in the grid's language, and any of them can be replaced through announcer.

StringSpoken when
sorted(column, direction)A column became sorted
sortCleared()The sort was cleared
filtered(count)The filter changed, with the rows it left
page(page)The page changed
columnResized(column, width)A column was resized
columnMoved(column, position)A column was moved
columnPinned(column, side)A column was pinned or unpinned
columnVisibility(column, hidden)A column was hidden or shown
groupCollapsed(group, collapsed)A header group was folded or unfolded
selected(count)The selection changed, with its size
copied(count)Rows were copied to the clipboard
rowExpanded(expanded)A nested row was expanded or collapsed
rowPinned(side)A row was pinned or unpinned
rowMoved(position)A row moved, with its new 1-based position
editInvalid(message)A commit was blocked by validation

Right to Left

Layout uses logical properties throughout, so dir="rtl" mirrors the grid rather than restyling it. Flip the switch below: Name is pinned to the start, so it moves to the right edge and its shadow turns with it, and the sort arrow and column menu follow.

The columns are wider than the demo, so drag the scrollbar in both directions too. Horizontal scrolling is normalized through scrollStart, because browsers disagree on whether scrollLeft is negative under RTL.

Role
Country
Hoang Kowalski
Design
Manager
Poland
$127,691.00
Bruno Nguyen
Growth
Support
Brazil
$105,146.00
Bruno Dubois
Design
Manager
Nigeria
$86,538.00
Farid Haddad
Growth
Analyst
Brazil
$76,443.00
Jonas Yilmaz
Core
Support
Vietnam
$129,812.00
Quyen Tanaka
Growth
Analyst
France
$72,011.00
6 rows
<!-- Layout uses logical properties throughout, so dir="rtl" mirrors the
     grid, pinned columns included. Horizontal scroll is normalized through
     scrollStart, since browsers disagree on the sign of scrollLeft under
     RTL. -->
<div dir="rtl">
  <DataGrid {grid} toolbar />
</div>