Accessibility pattern · Data display

Data grid

A grid is one stop in the Tab order; inside it the arrow keys move from cell to cell, as in a spreadsheet. Enter or F2 turns a cell into a field named by its row and column, Escape puts the old value back, and the totals that change are announced.

WCAG criteria
7
Keyboard rules
8
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Household budget

July to September, in rupees

Move with the arrow keys. Press Enter or F2 to edit an amount, Enter to save and Esc to cancel.

Category July August September Quarter
Rent ₹18,000 ₹18,000 ₹18,000 ₹54,000
Groceries ₹9,200 ₹8,750 ₹10,400 ₹28,350
Electricity ₹2,850 ₹3,100 ₹2,400 ₹8,350
Internet and phone ₹1,499 ₹1,499 ₹1,499 ₹4,497
Transport ₹3,600 ₹4,150 ₹3,900 ₹11,650
Eating out ₹4,500 ₹3,200 ₹6,800 ₹14,500
Total ₹39,649 ₹38,699 ₹42,999 ₹1,21,347

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves into the grid, onto the last cell used; the next Tab leaves the grid.
Arrow Right or Arrow Left or Arrow Down or Arrow UpMoves one cell in that direction, stopping at the edges.
Home or EndMoves to the first or last cell in the row.
CtrlHome or CtrlEndMoves to the first cell of the grid, or the last.
Enter or F2On an amount, opens it for editing with the value selected.
0–9On an amount, starts editing with that digit in place of the old value.
EnterWhile editing, saves the amount, updates the totals and puts focus back on the cell.
EscapeWhile editing, cancels and puts the old amount back.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the gridHousehold budget, grid. Rent, July, ₹18,000
Arrow Down moves to the next rowGroceries, ₹9,200
Enter opens the cell for editingGroceries July, edit text, selected 9200
Enter saves 9500Saved. July total: ₹39,949. Grand total: ₹1,21,647.
A letter is typed and Enter pressedEnter an amount in rupees, using digits only.
Focus reaches a totalTotal, Quarter, ₹1,21,347, read only

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-dgrid" data-ap-dgrid>
  <div class="ap-dgrid__top">
    <h3 class="ap-dgrid__name" id="dgrid-name">Household budget</h3>
    <p class="ap-dgrid__sub">July to September, in rupees</p>
    <p class="ap-dgrid__hint" id="dgrid-hint">Move with the arrow keys. Press <kbd>Enter</kbd> or <kbd>F2</kbd> to edit an amount, <kbd>Enter</kbd> to save and <kbd>Esc</kbd> to cancel.</p>
  </div>
  <div class="ap-dgrid__scroll">
    <table class="ap-dgrid__grid" role="grid" aria-labelledby="dgrid-name" aria-describedby="dgrid-hint">
      <thead>
        <tr aria-rowindex="1">
          <th scope="col" aria-colindex="1" tabindex="-1">Category</th>
          <th scope="col" aria-colindex="2" id="dgrid-c2" tabindex="-1">July</th>
          <th scope="col" aria-colindex="3" id="dgrid-c3" tabindex="-1">August</th>
          <th scope="col" aria-colindex="4" id="dgrid-c4" tabindex="-1">September</th>
          <th scope="col" aria-colindex="5" tabindex="-1">Quarter</th>
        </tr>
      </thead>
      <tbody>
        <tr aria-rowindex="2">
          <th scope="row" aria-colindex="1" id="dgrid-r2" tabindex="-1">Rent</th>
          <td aria-colindex="2" tabindex="0">₹18,000</td>
          <td aria-colindex="3" tabindex="-1">₹18,000</td>
          <td aria-colindex="4" tabindex="-1">₹18,000</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹54,000</td>
        </tr>
        <tr aria-rowindex="3">
          <th scope="row" aria-colindex="1" id="dgrid-r3" tabindex="-1">Groceries</th>
          <td aria-colindex="2" tabindex="-1">₹9,200</td>
          <td aria-colindex="3" tabindex="-1">₹8,750</td>
          <td aria-colindex="4" tabindex="-1">₹10,400</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹28,350</td>
        </tr>
        <tr aria-rowindex="4">
          <th scope="row" aria-colindex="1" id="dgrid-r4" tabindex="-1">Electricity</th>
          <td aria-colindex="2" tabindex="-1">₹2,850</td>
          <td aria-colindex="3" tabindex="-1">₹3,100</td>
          <td aria-colindex="4" tabindex="-1">₹2,400</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹8,350</td>
        </tr>
        <tr aria-rowindex="5">
          <th scope="row" aria-colindex="1" id="dgrid-r5" tabindex="-1">Internet and phone</th>
          <td aria-colindex="2" tabindex="-1">₹1,499</td>
          <td aria-colindex="3" tabindex="-1">₹1,499</td>
          <td aria-colindex="4" tabindex="-1">₹1,499</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹4,497</td>
        </tr>
        <tr aria-rowindex="6">
          <th scope="row" aria-colindex="1" id="dgrid-r6" tabindex="-1">Transport</th>
          <td aria-colindex="2" tabindex="-1">₹3,600</td>
          <td aria-colindex="3" tabindex="-1">₹4,150</td>
          <td aria-colindex="4" tabindex="-1">₹3,900</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹11,650</td>
        </tr>
        <tr aria-rowindex="7">
          <th scope="row" aria-colindex="1" id="dgrid-r7" tabindex="-1">Eating out</th>
          <td aria-colindex="2" tabindex="-1">₹4,500</td>
          <td aria-colindex="3" tabindex="-1">₹3,200</td>
          <td aria-colindex="4" tabindex="-1">₹6,800</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹14,500</td>
        </tr>
      </tbody>
      <tfoot>
        <tr aria-rowindex="8">
          <th scope="row" aria-colindex="1" tabindex="-1">Total</th>
          <td aria-colindex="2" aria-readonly="true" tabindex="-1">₹39,649</td>
          <td aria-colindex="3" aria-readonly="true" tabindex="-1">₹38,699</td>
          <td aria-colindex="4" aria-readonly="true" tabindex="-1">₹42,999</td>
          <td aria-colindex="5" aria-readonly="true" tabindex="-1">₹1,21,347</td>
        </tr>
      </tfoot>
    </table>
  </div>
  <p class="ap-dgrid__error" id="dgrid-error" role="alert"></p>
  <p class="ap-dgrid__status" role="status"></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The grid is a real table: column and row headers name every cell, and aria-rowindex and aria-colindex give each one its position.

  • 2.1.1 Keyboard Level A

    Every cell is reached with the arrow keys, and editing, saving and cancelling all work from the keyboard.

  • 2.4.3 Focus Order Level A

    The grid is one tab stop, and focus goes back to the edited cell after saving or cancelling.

  • 2.4.7 Focus Visible Level AA

    The focused cell has a two-pixel ring and a tinted fill, and its row and column headers light up.

  • 3.3.1 Error Identification Level A

    An amount that is not a number keeps the field open, marks it invalid and says what to type in an alert.

  • 4.1.2 Name, Role, Value Level A

    The roles come from role=grid on the table, totals carry aria-readonly, and the edit field is named by its row and column headers.

  • 4.1.3 Status Messages Level AA

    The new column total and grand total are announced politely after each save, without moving focus.

Usage

When to use it

Use it

  • Values people change cell by cell, such as a budget, a timesheet or stock counts.
  • Dense data people move around with the keyboard, the way they would in a spreadsheet.

Use something else

  • Data people only read, sort or select: a table keeps the reading commands screen reader users rely on.
  • Forms with a handful of fields: plain labelled inputs are simpler for everyone.
  • Layout: never use role=grid to arrange cards or buttons in rows.

Common failures

How it usually goes wrong

  • Every cell in the Tab order

    Forty cells means forty Tab presses to get past the grid. One tab stop, with arrow keys inside, is what people expect from a grid.

  • role=grid on a table people only read

    A grid switches screen readers into focus mode, which turns off the commands for reading a table. Use a plain table unless the cells are interactive.

  • An edit field with no name

    An input that appears in a cell is announced as just "edit text". This one is labelled by its row and column headers: Groceries July.

  • Escape that throws the value away

    If Escape clears the cell, a stray key press loses data. Here the old amount is kept and put back.

  • Totals that change silently

    Recalculated totals may be off screen or far from focus. A polite status message says the new totals after each save.

  • Focus dropped after saving

    Removing the edit field sends focus to the top of the page unless it is moved. It goes back to the cell that was edited.

Notes

Building it

  • Focus sits on the cell itself, with a roving tabindex; if a cell held a button or a link, focus would go to that instead.
  • aria-rowindex and aria-colindex matter when only part of a large grid is in the page; with every row present they confirm each cell's place.
  • Typing a digit replaces the amount, as spreadsheets do; Enter or F2 keeps it and selects it, so either habit works.
  • The edit field is type=text with inputmode=numeric: phones show a number pad, and a typed rupee sign or comma is simply ignored.
  • Ctrl+Home and Ctrl+End scroll the page in most browsers, so the grid stops those keys only while a cell has focus.

Sources: WAI-ARIA Authoring Practices: Grid · WAI-ARIA Authoring Practices: Data Grid Examples

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction