The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a data table<table> <tr> <th>Plan</th> <th>Price</th> </tr> <tr> <td>Basic</td> <td>$9</td> </tr> </table>View examples
Name the table<table> <caption>Monthly subscriptions</caption> <tr> <th>Plan</th> <th>Users</th> </tr> </table>View examples
Group header rows<thead> <tr> <th scope="col">Product</th> <th scope="col">Sales</th> </tr> </thead>View examples
Group body rows<tbody> <tr> <th scope="row">Keyboard</th> <td>42</td> </tr> </tbody>View examples
Group summary rows<tfoot> <tr> <th scope="row">Total</th> <td>42</td> </tr> </tfoot>View examples
Mark a column header<th scope="col">Revenue</th>View examples
Mark a row header<th scope="row">January</th>View examples
Expand a short header<th scope="col" abbr="Quantity">Qty</th>View examples
Span header columns<th scope="colgroup" colspan="2">First half</th>View examples
Span header rows<th scope="rowgroup" rowspan="2">Europe</th>View examples
Group table columns<colgroup> <col class="label-column"> <col span="2" class="number-column"> </colgroup>View examples
Identify a complex header<th id="q1-sales">Q1 sales</th>View examples
Associate several headers<td headers="europe q1-sales">120</td>View examples
Expose sort direction<th scope="col" aria-sort="ascending"> <button type="button">Name</button> </th>View examples

Use tables for genuinely tabular relationships, not page layout. Start with a caption, mark every header with th, organize long tables into row groups, and keep header relationships simple enough that scope can describe them before reaching for explicit IDs and headers attributes.

Step by step

Detailed examples

01

Start with rows, cells, and a caption

A table represents data whose meaning depends on row and column relationships. Put a caption directly inside table so users can identify that data set, use th for labels, and use td for values. Avoid a table when a list, definition list, or ordinary layout communicates the content more clearly.

Simple subscription table
<table>
  <caption>Monthly subscriptions</caption>
  <tr>
    <th scope="col">Plan</th>
    <th scope="col">Price</th>
  </tr>
  <tr>
    <td>Basic</td>
    <td>$9</td>
  </tr>
</table>
Back to quick reference ↑
02

Organize header, body, and footer rows

thead, tbody, and tfoot communicate structural row groups and give CSS stable styling hooks. They do not replace individual header cells: cells that label data still need th. A table may have multiple tbody groups, but its rows must form a valid rectangular grid.

Sales table with a total
<table>
  <caption>Units sold</caption>
  <thead><tr><th scope="col">Product</th><th scope="col">Sales</th></tr></thead>
  <tbody><tr><th scope="row">Keyboard</th><td>42</td></tr></tbody>
  <tfoot><tr><th scope="row">Total</th><td>42</td></tr></tfoot>
</table>
Back to quick reference ↑
03

Describe straightforward header relationships

For a simple table, scope col and scope row make the intended relationship explicit and resilient. The abbr attribute can provide a shorter or expanded alternative for a header, but the visible header should remain understandable on its own.

Monthly inventory headers
<table>
  <caption>Inventory by month</caption>
  <thead><tr><th scope="col">Month</th><th scope="col" abbr="Quantity">Qty</th><th scope="col">Revenue</th></tr></thead>
  <tbody><tr><th scope="row">January</th><td>18</td><td>$540</td></tr></tbody>
</table>
Back to quick reference ↑
04

Use spans only for real header groups

colspan and rowspan change the table grid and are useful for grouped headings. Match their numeric spans to the actual cells covered and use colgroup or rowgroup scope on group headers. Excessive spanning makes both responsive presentation and header associations harder to understand.

Quarterly sales by region
<table>
  <caption>Regional sales</caption>
  <thead>
    <tr><th rowspan="2" scope="col">Region</th><th colspan="2" scope="colgroup">First half</th></tr>
    <tr><th scope="col">Q1</th><th scope="col">Q2</th></tr>
  </thead>
  <tbody>
    <tr><th rowspan="2" scope="rowgroup">Europe</th><td>120</td><td>135</td></tr>
    <tr><td>118</td><td>142</td></tr>
  </tbody>
</table>

Note: Test complex spanning structures with the screen readers and browsers your audience uses.

Back to quick reference ↑
05

Attach styling hooks to columns

colgroup and col describe columns without adding repeated classes to every cell. They support a limited set of presentational effects through CSS, and they do not create semantic headers or accessible names by themselves.

Label and numeric column groups
<table>
  <caption>Project budget</caption>
  <colgroup>
    <col class="label-column">
    <col span="2" class="number-column">
  </colgroup>
  <tr><th scope="col">Team</th><th scope="col">Budget</th><th scope="col">Actual</th></tr>
  <tr><th scope="row">Design</th><td>$20k</td><td>$18k</td></tr>
</table>
Back to quick reference ↑
06

Associate complex cells explicitly

When scope cannot express a genuinely complex, irregular relationship, give each relevant th a unique id and list those IDs in the data cell's headers attribute. This is verbose and easy to break during edits, so simplify the table or split it into smaller tables when practical.

Explicit region and quarter headers
<table>
  <caption>Quarterly sales</caption>
  <tr><th></th><th id="q1-sales">Q1 sales</th></tr>
  <tr><th id="europe">Europe</th><td headers="europe q1-sales">120</td></tr>
</table>
Back to quick reference ↑
07

Expose sortable table state

Put aria-sort only on the header for the column currently controlling sort order, and update it after each sort. A real button provides keyboard interaction and an accessible action name; aria-sort alone does not implement sorting behavior.

Ascending name column
<table>
  <caption>Team members</caption>
  <thead><tr><th scope="col" aria-sort="ascending"><button type="button">Name</button></th><th scope="col">Role</th></tr></thead>
  <tbody><tr><td>Ada</td><td>Engineer</td></tr></tbody>
</table>

Note: JavaScript must update both row order and aria-sort when the user activates a sort button.

Back to quick reference ↑

Local code tester

Try an accessible data table

Edit the caption, headers, row groups, and values, then render the table locally.

Runs in your browser
Preview

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. WHATWGHTML Standard — Tabular datahtml.spec.whatwg.org
  2. MDN Web DocsHTML table accessibilitydeveloper.mozilla.org
  3. MDN Web DocsThe th elementdeveloper.mozilla.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback