Table Structure & Caption

A proper table is a grid of rows and columns with a caption for the accessible name and header cells that tell screen readers how every data cell relates to its row and column.

HTML6 min readConcept 23 of 42

What it is

An HTML table starts with <table> and is built from rows (<tr>) that contain either header cells (<th>) or data cells (<td>). Rows are grouped into three semantic sections: <thead> for column headers, <tbody> for the main data, and <tfoot> for summary rows like totals. <caption> is an optional but important element that sits as the very first child of <table> and provides a visible title.

<th> cells accept a scope attribute that declares the direction of the header: scope="col" means this cell is the header for its entire column, and scope="row" means it is the header for its entire row. This association is what lets assistive technology read a cell and immediately announce which column header and row header it belongs to.

Why it matters

<caption> is the table's accessible name. When a screen reader user encounters a table, the caption is the first thing announced, giving context before a single data cell is read. Without a caption, the table is announced as a generic table and the user has no immediate way to know what the data is about. A good caption is one sentence that names the data: not "Table 1" but "Monthly server uptime by region".

The scope attribute on <th> is what turns a visual grid into a navigable structure. Screen readers use scope associations to read a cell as "Row: Japan, Column: Q3, Value: 98.2%" rather than just "98.2%". Without scope, the column context is lost and multi-column tables become very hard to parse by ear.

How it works

Build the table top to bottom: <caption>, then <thead>, then <tbody>, then <tfoot> if you have summary rows. Put column headers in <th scope="col"> inside the <thead>. Put row headers (if the first column labels the row) in <th scope="row"> inside each <tbody> row. Every other cell is <td>.

For styling, reach for CSS rather than the deprecated border, cellpadding, and cellspacing attributes. <colgroup> and <col> let you style entire columns without adding classes to every cell. A <col> inside <colgroup> targets the column at that index: <colgroup><col><col style="background: var(--highlight)"></colgroup> highlights the second column.

A <tfoot> row can hold totals or averages. It is placed after <tbody> in the source but browsers have traditionally rendered it at the bottom of the table regardless of source order (this behaviour is now deprecated; just put <tfoot> last).

Try it

Watch the table assemble in order: caption first, then the header row, then each data row in turn.

<table>  <caption>Weekly grocery run</caption>  <thead>    <tr>      <th scope="col">Item</th>      <th scope="col">Qty</th>      <th scope="col">Price</th>    </tr>  </thead>  <tbody>    <tr>      <th scope="row">Oats</th>      <td>2</td><td>$3.49</td>    </tr>    <tr>      <th scope="row">Milk</th>      <td>1</td><td>$2.79</td>    </tr>    <tr>      <th scope="row">Eggs</th>      <td>12</td><td>$4.99</td>    </tr>  </tbody>  <tfoot>    <tr>      <th scope="row">Total</th>      <td>15</td><td>$11.27</td>    </tr>  </tfoot></table>

Anatomy of a table

captionFirst child of table. Accessible name announced by screen readers before any cell.
theadColumn header rows. Use scope="col" on each <th>.
tbodyData rows. Row header cells use scope="row".
tfootSummary rows (totals, averages). Last child of table.

Table assembles row by row

Weekly grocery run
ItemQtyPrice
Oats2$3.49
Milk1$2.79
Eggs12$4.99
Total15$11.27

scope tells screen readers the direction

scope="col" on a top-row <th> says: "I am the header for everything below me in this column."
scope="row" on a left-column <th> says: "I am the header for everything to my right in this row."
Without scope, a screen reader cannot tell which direction a header applies. A cell reads as "98.2%" with no row or column label.

Check yourself

Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.

  1. 1What is the accessible role of <caption> on a table?
  2. 2A table's first column contains country names. What scope value should those <th> cells have?
  3. 3What is the correct order of structural sections inside a <table>?

Remember this

  • <caption> is the first child of <table> and provides the table's accessible name.
  • <th scope="col"> labels a column; <th scope="row"> labels a row; both are needed for screen reader navigation.
  • <thead>, <tbody>, <tfoot> group rows semantically and enable CSS tricks like sticky column headers.
  • Use tables for tabular data only; use CSS Grid or Flexbox for page layout.

Done with this concept?

Mark it complete to track your progress. No login needed.