Overview

A <table> is for data with a real row-and-column relationship: schedules, comparison matrices, specifications, financial figures. It is not a layout tool; use css-grid or css-flexbox for that. A screen reader announces a layout table as “table, 3 columns, 12 rows” and reads cells in source order, which is noise. The checklist is html.

Use a table when each cell has meaning from its row and column

If a value is only understandable with both its row header and its column header, it is tabular. Navigation bars, page columns, and card galleries are not.

Name the table with <caption>

Put <caption> first inside <table>; it is the native accessible name. Do not substitute aria-label. If a visible heading already says the same thing, hide the caption visually with an .sr-only class rather than removing it.

Group rows with <thead>, <tbody>, and <tfoot>

<thead> holds header rows, <tbody> holds data, and <tfoot> holds summary rows. Browsers and assistive technology use the structure to group and navigate rows.

Mark headers with <th scope>

Use <th> for every header, never a styled <td>. Add scope="col" to column headers and scope="row" to row headers so assistive technology associates each cell with the right headers. Use colgroup and rowgroup for headers that span several columns or rows.

<table>
  <caption>Q1 2026 revenue by region</caption>
  <thead>
    <tr><th scope="col">Region</th><th scope="col">Revenue</th><th scope="col">Growth</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">APAC</th><td>$4.2M</td><td>+12%</td></tr>
    <tr><th scope="row">EMEA</th><td>$3.8M</td><td>+8%</td></tr>
  </tbody>
  <tfoot>
    <tr><th scope="row">Total</th><td>$8.0M</td><td>+10%</td></tr>
  </tfoot>
</table>

Wire complex tables with id and headers

When a cell has more than one row header and one column header, scope is not enough. Give each header an id and list the ids on the data cell with headers.

<td headers="q1 apac">$4.2M</td>

Prefer splitting a multi-level table into simpler tables with their own captions; complex tables are hard to read visually and to make accessible. Every spanning header (colspan, rowspan) needs a matching scope="colgroup" or rowgroup, or explicit headers wiring.

Keep layout out of tables

.layout { display: grid; grid-template-columns: 1fr 300px; }

For custom grid widgets that need ARIA roles, see html-aria-patterns.