Skip to content

Find what you need, instantly

Search components, ARIA roles & attributes, and WCAG criteria.

Data Display

Grid (interactive data grid)

An interactive tabular widget with two-dimensional arrow-key cell navigation (role="grid"). Use only when cells are interactive — read-only data belongs in a Table.

Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-08.

Implementation

Grid or Table?

Reach for the grid pattern only when cells are interactive — editable values, selectable cells, or cells that contain their own controls — so the widget needs to own two-dimensional arrow-key navigation. For static data the user only reads, use a Table instead: a real <table> keeps the screen reader's own table-navigation keys working and needs none of this keyboard code. A grid has no native HTML element, so unlike a table there is no simpler markup to fall back to — every role and key is on you.

Region
Q1
Q2
Q3
Q4
North America
820
910
880
1020
Europe
610
640
700
760
Asia Pacific
430
520
610
690
Latin America
180
210
240
260

Tab once to enter the grid, then use the arrow keys (plus Home / End, Ctrl+Home / Ctrl+End) to move between cells.

grid-pattern.tsx (roving tabindex + arrow keys)
// One cell owns tabindex=0; every other cell is tabindex=-1 (roving tabindex),
// so the whole grid is a SINGLE Tab stop and arrow keys drive navigation.
const [active, setActive] = useState({ row: 0, col: 0 });

function onKeyDown(e, row, col) {
  let r = row, c = col;
  switch (e.key) {
    case "ArrowRight": c = Math.min(col + 1, COL_COUNT - 1); break;
    case "ArrowLeft":  c = Math.max(col - 1, 0); break;
    case "ArrowDown":  r = Math.min(row + 1, ROW_COUNT - 1); break;
    case "ArrowUp":    r = Math.max(row - 1, 0); break;
    case "Home":       c = 0; if (e.ctrlKey) r = 0; break;             // start of row / grid
    case "End":        c = COL_COUNT - 1; if (e.ctrlKey) r = ROW_COUNT - 1; break;
    default: return;   // Tab, typing, etc. behave normally
  }
  e.preventDefault();
  setActive({ row: r, col: c });
  cellRefs.current[r][c].focus();   // move DOM focus to match the roving state
}

<div role="grid" aria-label="Quarterly revenue by region" aria-readonly="true">
  <div role="row">
    <div role="columnheader">Region</div>
    <div role="columnheader">Q1</div>
    {/* … */}
  </div>
  <div role="row">
    <div role="rowheader" tabIndex={isActive ? 0 : -1} onKeyDown={…}>North America</div>
    <div role="gridcell" tabIndex={isActive ? 0 : -1} onKeyDown={…}>820</div>
    {/* … */}
  </div>
</div>

Live demo

Region
Q1
Q2
Q3
Q4
North America
820
910
880
1020
Europe
610
640
700
760
Asia Pacific
430
520
610
690
Latin America
180
210
240
260

Tab once to enter the grid, then use the arrow keys (plus Home / End, Ctrl+Home / Ctrl+End) to move between cells.

Required roles, states & properties

Required roles, states, and properties
ElementAttributeWhy
Grid containerrole="grid"Declares an interactive composite widget so assistive tech switches out of document-reading mode and lets the app's own arrow-key handling drive cell navigation. This is the one thing that distinguishes a grid from a static table.
Grid containeraria-label / aria-labelledbyGives the whole widget an accessible name (a grid has no <caption>), announced when focus first enters it.
Grid containeraria-readonly="true"Declares that cells display but cannot be edited. Omit it (or set false) for an editable grid where cells accept input.
Each rowrole="row"Rebuilds the row grouping that div markup loses, so a cell is announced with its row context.
Header cellsrole="columnheader" / role="rowheader"Associates each data cell with its column and row labels, so navigating to a cell announces e.g. "North America, Q3, 880" rather than a bare number.
Data cellsrole="gridcell"Marks the individual navigable cells that make up the grid's two-dimensional structure.
Cells (roving tabindex)tabindex="0" on the active cell, tabindex="-1" on all othersMakes the grid a single stop in the page Tab order; the arrow keys then move the single tabindex=0 among cells and set DOM focus to match, instead of exposing dozens of separate tab stops.

Keyboard interaction model

Keyboard interaction model
KeyBehavior
Tab / Shift+TabMoves into and out of the grid as a whole — the entire grid is a single tab stop. Focus lands on whichever cell was last active (initially the first cell).
Right / Left ArrowMoves focus one cell right / left within the current row, stopping at the row's edge (no wrap).
Down / Up ArrowMoves focus one cell down / up within the current column, stopping at the grid's edge.
Home / EndMoves focus to the first / last cell in the current row.
Ctrl + Home / Ctrl + EndMoves focus to the first cell of the first row / the last cell of the last row.

This is the core of the pattern: the grid takes a single Tab stop, and the arrow keys (plus Home/End and their Ctrl variants) move a roving tabindex=0 among the cells. A plain Table deliberately does not do this — arrow keys there belong to the screen reader, not the app.

Focus management rules

  • Exactly one cell has tabindex=0 at any moment; every other cell is tabindex=-1. This is what makes the whole grid a single tab stop.
  • Every arrow/Home/End key press updates both the roving tabindex and actual DOM focus in the same handler, so they never drift apart.
  • Focus stops at the grid's edges — arrow keys do not wrap around to the opposite side.
  • When focus leaves and later returns via Tab, it lands on the last-active cell, not a reset to the first cell.

WCAG 2.2 success criteria mapping

WCAG 2.2 success criteria that apply to this component
SCNameLevelWhy it applies
1.3.1Info and RelationshipsAStructure and relationships conveyed visually are also programmatically determinable.
2.1.1KeyboardAAll functionality is operable through a keyboard interface with no specific timing.
2.4.3Focus OrderAFocusable components receive focus in an order that preserves meaning and operability.
2.4.7Focus VisibleAAAny keyboard-operable UI has a visible focus indicator.
4.1.2Name, Role, ValueAFor all UI components, name, role, and value are programmatically determinable; states and changes are announced.

References