Data Display

Grid

Available

An interactive table (data grid) where individual cells are keyboard-navigable.

Specification updated

What Is a Grid (Data Grid)?

A grid is an interactive UI that lets users navigate tabular data freely using arrow keys. Like a spreadsheet, you can move focus from cell to cell to interact with the data.

This kind of grid is different from the CSS layout grid. Also, if you just need to display data in a table, use a plain <table> — there's no need for a grid. Only use a grid when interaction is required, such as when you need cell-level focus movement or cells contain interactive elements.

Why Does Accessibility Matter?

The more interactive a table becomes, the more people get locked out when it's built incorrectly.

The solution is to declare the structure with role="grid" and move focus with arrow keys (roving tabindex). The entire grid becomes a single tab stop, and navigation within it is handled by arrow keys.

Live Demo (Recommended Implementation)

The employee directory below is an interactive grid built following the APG.Without using a mouse, press Tab to enter the grid, then use arrow keys to move between cells.

Accessible Data Grid

Employee Directory

Employee IDNameDepartmentExtension
E-001Hanako SatoSales1201
E-002Taro SuzukiEngineering1310
E-003Misaki TakahashiHuman Resources1105
E-004Ken TanakaEngineering1322

Try it: Tab to enter the grid → ↑ ↓ ← → to move between cells → Home / End for the first/last cell in a row, Ctrl + Home / Ctrl + End for the first/last cell in the grid.

Tip

Turn on a screen reader (on macOS, press +F5 for VoiceOver) and enter a cell. You'll hear something like "grid, 4 columns, 5 rows" describing the structure, and as you move, "Name column, Hanako Sato" announcingthe column header and cell value. This is the effect of role="grid" and column headers.

Keyboard Interaction

KeyActionPriority
TabEnter / exit the grid (the grid is a single tab stop)Required
/ Move focus to the right / left cellRequired
/ Move focus to the cell below / aboveRequired
Home / EndMove to the first / last cell in the current rowRequired
Ctrl + Home / Ctrl + EndMove to the first / last cell in the gridRecommended

Note

Do not use Tab for cell navigation within the grid.Tab should be reserved for leaving the grid, while cell navigation is handled by arrow keys. This is the key difference from a native table.

Required WAI-ARIA Roles and Properties

TargetAttribute / RoleMeaning
Outer wrapperrole="grid"Declares the element as an interactive grid.
Outer wrapperaria-label / aria-labelledbyProvides an accessible name for the grid (describes what the table represents).
Each rowrole="row"Identifies a row grouping.
Header cellsrole="columnheader"Column header. Read in association with cells in the same column.
Data cellsrole="gridcell"An individual cell.
Focusable celltabindex="0" (one only) / "-1" (all others)Roving tabindex. Makes the grid a single tab stop; swap tabindex values as focus moves.

Implementation: Recommended Pattern (Good)

Good / Recommended

Declare the structure with role="grid", and keep only one cell at tabindex="0"at a time, moving focus with arrow keys (roving tabindex).

Markup:

<div role="grid" aria-labelledby="grid-cap">
  <div role="row">
    <span role="columnheader" tabindex="-1">Employee ID</span>
    <span role="columnheader" tabindex="-1">Name</span>
    <span role="columnheader" tabindex="-1">Department</span>
    <span role="columnheader" tabindex="-1">Extension</span>
  </div>
  <div role="row">
    <!-- Only the first cell in the entire grid has tabindex="0" (the rest are -1) -->
    <span role="gridcell" tabindex="0">E-001</span>
    <span role="gridcell" tabindex="-1">Hanako Sato</span>
    <span role="gridcell" tabindex="-1">Sales</span>
    <span role="gridcell" tabindex="-1">1201</span>
  </div>
  <!-- Subsequent rows also use role="row" + role="gridcell" tabindex="-1" -->
</div>

Keyboard interaction script (focus movement and tabindex management):

const grid = document.getElementById('grid-good');
if (grid) {
  // Represent cells as a 2D array (array of cells per row)
  const rows = Array.from(grid.querySelectorAll<HTMLElement>('[role="row"]'));
  const cells = rows.map((row) =>
    Array.from(row.querySelectorAll<HTMLElement>('[role="columnheader"],[role="gridcell"]'))
  );

  let r = 0; // Current row
  let c = 0; // Current column

  // Set tabindex="0" only on the current cell, -1 on all others (single tab stop)
  function focusCell(nextR: number, nextC: number) {
    const maxR = cells.length - 1;
    r = Math.max(0, Math.min(nextR, maxR));
    const maxC = cells[r].length - 1;
    c = Math.max(0, Math.min(nextC, maxC));
    cells.flat().forEach((cell) => cell.setAttribute('tabindex', '-1'));
    const target = cells[r][c];
    target.setAttribute('tabindex', '0');
    target.focus();
  }

  grid.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'ArrowRight': focusCell(r, c + 1); break;
      case 'ArrowLeft':  focusCell(r, c - 1); break;
      case 'ArrowDown':  focusCell(r + 1, c); break;
      case 'ArrowUp':    focusCell(r - 1, c); break;
      case 'Home':
        if (e.ctrlKey) focusCell(0, 0);            // First cell
        else focusCell(r, 0);                       // First cell in row
        break;
      case 'End':
        if (e.ctrlKey) focusCell(cells.length - 1, // Last cell
          cells[cells.length - 1].length - 1);
        else focusCell(r, cells[r].length - 1);    // Last cell in row
        break;
      default: return; // Let the browser handle other keys
    }
    e.preventDefault();
  });

  // Sync current position when a cell is clicked
  cells.forEach((row, ri) =>
    row.forEach((cell, ci) => {
      cell.addEventListener('click', () => focusCell(ri, ci));
    })
  );
}

Note

If cells contain buttons or links, first ensure arrow key navigation between cells works, then consider adding an "edit mode" entered via Enter or F2 to interact with elements inside the cell. Start by making the one cell = one focus target basic pattern work reliably.

Anti-pattern (Bad)

Below is a table where cells only have onclick — it looks interactive but isn't.You can click cells with a mouse, but keyboard interaction is completely impossible.Try Tab → arrow keys and compare the experience to the demo above.

Broken grid built with onclick only
Employee IDNameDepartmentExtension
E-001Hanako SatoSales1201
E-002Taro SuzukiEngineering1310
E-003Misaki TakahashiHuman Resources1105
E-004Ken TanakaEngineering1322

Try it: Tab doesn't focus any cell, and arrow keys don't work. A screen reader won't recognize it as a grid, and row/column/position information isn't communicated.

<!-- ❌ Anti-pattern -->
<!-- Looks like a table but doesn't function as an interactive grid -->
<table>
  <tr>
    <th>Employee ID</th><th>Name</th><th>Department</th><th>Extension</th>
  </tr>
  <tr>
    <!-- onclick only. No role, tabindex, or key handling -->
    <td onclick="select(this)">E-001</td>
    <td onclick="select(this)">Hanako Sato</td>
    <td onclick="select(this)">Sales</td>
    <td onclick="select(this)">1201</td>
  </tr>
</table>

Bad / Avoid

Problems with this implementation:

  • No keyboard access — Cells have no tabindex or key handling, so arrow keys don't work.
  • Not recognized as a grid — Without role="grid", assistive technology treats it as a plain table.
  • Not a single tab stop — Roving tabindex pattern is not implemented.
  • Position not communicated — Current row, column, and selection state are not announced.

Tip

If your table doesn't need click or keyboard interaction, don't force it into a grid. A native <table> with <th scope> is perfectly fine. Only introduce role="grid" and roving tabindex when cell-level interaction is truly needed.

Implementation Checklist


Source (English):Grid Pattern — W3C APG(opens in a new tab)