APG Patterns
ๆ—ฅๆœฌ่ชž
ๆ—ฅๆœฌ่ชž
๐Ÿ“Š

Grid and Table Properties

To fully convey the structure and relationships in a grid or table, authors need to ensure ARIA row and column properties are correctly set.

This practice is documented in detail at Grid and Table Properties - WAI-ARIA APG . Below we provide additional context specific to our pattern implementations.

Overview

Grids and tables are used to present structured data. For screen reader users to navigate and understand this data, proper ARIA properties must convey the structureโ€”row and column counts, headers, and cell relationships. This practice explains how to set these properties correctly.

When to Use Table vs Grid

PatternUse WhenKeyboard Behavior
tableStatic, read-only dataStandard tab navigation
gridInteractive cells (editable, actionable)Arrow key navigation between cells
treegridHierarchical data with expandable rowsArrow keys + expand/collapse

Essential ARIA Properties

Row and Column Counts

When using virtual scrolling or showing partial data:

<div role="grid" aria-rowcount="1000" aria-colcount="5">
  <!-- Only showing rows 10-20 -->
  <div role="row" aria-rowindex="10">
    <div role="gridcell" aria-colindex="1">...</div>
  </div>
</div>
PropertyPurpose
aria-rowcountTotal number of rows (including hidden)
aria-colcountTotal number of columns (including hidden)
aria-rowindexCurrent rowโ€™s position (1-based)
aria-colindexCurrent cellโ€™s column position (1-based)

Spanning Cells

For cells that span multiple rows or columns:

<div role="gridcell" aria-colspan="2">Spans 2 columns</div>
<div role="gridcell" aria-rowspan="3">Spans 3 rows</div>

Headers

Use columnheader and rowheader roles:

<div role="grid">
  <div role="row">
    <div role="columnheader">Name</div>
    <div role="columnheader">Email</div>
  </div>
  <div role="row">
    <div role="rowheader">John Doe</div>
    <div role="gridcell">john@example.com</div>
  </div>
</div>

Native HTML Table Enhancement

When using native <table> elements, most properties are implicit:

<table>
  <thead>
    <tr>
      <th scope="col">Product</th>
      <!-- columnheader -->
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Widget</th>
      <!-- rowheader -->
      <td>$10.00</td>
      <!-- cell -->
    </tr>
  </tbody>
</table>

Add ARIA only when needed (virtual scrolling, dynamic updates):

<table aria-rowcount="500">
  <tbody>
    <tr aria-rowindex="50">
      ...
    </tr>
  </tbody>
</table>

Implementation Tips

Sort State

Indicate sortable columns and current sort:

<div role="columnheader" aria-sort="ascending">Name โ†‘</div>
<div role="columnheader" aria-sort="none">Email</div>
ValueMeaning
ascendingSorted A-Z, low to high
descendingSorted Z-A, high to low
noneSortable but not currently sorted
otherSorted by other algorithm

Read-only and Disabled States

<!-- Read-only cell -->
<div role="gridcell" aria-readonly="true">Fixed value</div>

<!-- Disabled cell -->
<div role="gridcell" aria-disabled="true">Unavailable</div>

Selection State

For grids with selectable rows or cells:

<div role="grid" aria-multiselectable="true">
  <div role="row" aria-selected="true">Selected row</div>
  <div role="row" aria-selected="false">Not selected</div>
</div>

Common Pitfalls

Missing Row/Column Indices

When using virtual scrolling, always provide indices so screen readers know the cellโ€™s position:

<!-- Bad: No position information -->
<div role="row">...</div>

<!-- Good: Clear position -->
<div role="row" aria-rowindex="50">...</div>

Incorrect Scope in Native Tables

<!-- Bad: Missing scope -->
<th>Name</th>

<!-- Good: Explicit scope -->
<th scope="col">Name</th>
<th scope="row">John</th>

Using Grid for Non-interactive Tables

If users donโ€™t need to interact with individual cells, use table role instead of grid to avoid unnecessary complexity in keyboard navigation.

Resources