Tables present data in rows and columns, where each value only makes sense together with its row and column headings: timetables, price lists, results, comparisons. HTML tables make those relationships explicit so everyone can follow them, including people who hear the table read aloud one cell at a time.
The four core elements
| Element | Meaning |
|---|---|
table | The table itself |
tr | A table row |
th | A header cell |
td | A data cell |
<table>
<tr>
<th>Loaf</th>
<th>Weight</th>
<th>Price</th>
</tr>
<tr>
<td>White</td>
<td>800g</td>
<td>£3.20</td>
</tr>
<tr>
<td>Sourdough</td>
<td>900g</td>
<td>£4.50</td>
</tr>
</table>
A table is built row by row. Each tr contains cells, and each row should have the same number of columns. The first row uses th cells, so the browser knows they are headings for the columns below them.
Why header cells matter
Visually, you glance up to see which column a value belongs to. A screen reader user cannot glance. When they move into a cell, the screen reader announces the relevant header first, for example "Price, £4.50". That only works if the headers are real th elements.
<!-- Looks like a header, but is not one -->
<tr>
<td><b>Loaf</b></td>
<td><b>Price</b></td>
</tr>
<!-- A real header row -->
<tr>
<th>Loaf</th>
<th>Price</th>
</tr>
Browsers show th text bold and centred by default. Change that with CSS if you like; keep the element.
Row headers
Sometimes the first cell of each row is the heading for that row. Make it a th too:
<table>
<tr>
<th>Day</th>
<th>Opens</th>
<th>Closes</th>
</tr>
<tr>
<th>Monday</th>
<td>7am</td>
<td>4pm</td>
</tr>
<tr>
<th>Saturday</th>
<td>8am</td>
<td>2pm</td>
</tr>
</table>
Now a screen reader can announce "Saturday, Closes, 2pm". In simple tables like this, browsers work out which header applies to which cell. Lesson 17 shows the scope attribute, which makes this explicit and is needed for anything more complex.
Grouping rows: thead, tbody and tfoot
Wrap the header rows in thead, the body rows in tbody and any summary rows in tfoot:
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>Order summary</title>
<style>
table { border-collapse: collapse; }
th, td { border: 1px solid #999; padding: 0.4rem 0.75rem; text-align: left; }
td:last-child { text-align: right; }
</style>
</head>
<body>
<table>
<caption>Your order</caption>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td>Croissant</td>
<td>4</td>
<td>£7.20</td>
</tr>
<tr>
<td>Sourdough</td>
<td>1</td>
<td>£4.50</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>Total</th>
<td></td>
<td>£11.70</td>
</tr>
</tfoot>
</table>
</body>
</html>
These groups help assistive technology, let you style the parts separately, and allow browsers to repeat the header on each printed page of a long table. If you leave out tbody, the browser adds one to the DOM anyway, which matters when you write CSS selectors such as table > tr, which will not match.
The caption gives the table a title. It must be the first child of the table. Lesson 17 covers it in more detail.
Styling tables
Browsers draw no borders by default. The CSS in the example above shows the usual starting point:
border-collapse: collapsemerges the borders of neighbouring cells into single lines.paddingon cells gives the data room to breathe.- Right-aligning numbers makes them easier to compare.
Avoid the old presentational attributes such as border, cellpadding, bgcolor and align. They are obsolete; use CSS.
When a table is the wrong tool
Tables are for data with relationships in two directions. Before CSS layout was capable, developers used tables to position page regions side by side. That causes real problems:
- Screen readers announce rows and columns that mean nothing.
- The reading order follows the table source, which may not match the visual order.
- Layout tables are hard to make responsive.
Use CSS Grid or Flexbox for layout. The CSS Complete Course covers both.
Other times a table is the wrong choice:
| Content | Better choice |
|---|---|
| A list of items with one property each | ul or ol |
| Name and value pairs, such as product specs | dl |
| A grid of product cards | A list of article elements laid out with CSS Grid |
| A form arranged in columns | Labels and inputs laid out with CSS |
If you would never sort or compare the content by column, it is probably not a table.
Wide tables overflow small screens. A simple fix is to wrap the table in a container with overflow-x: auto so it scrolls sideways without breaking the page. The Responsive Web Design course covers responsive tables in detail.
Common mistakes
| Mistake | Fix |
|---|---|
Bold td cells used as headers | Use th |
| Rows with different numbers of cells | Add empty td cells or use spanning (lesson 17) |
| Table used for page layout | Use CSS Grid or Flexbox |
| Empty cells used for spacing | Use CSS padding |
Text in the table explaining what it is, with no caption | Add a caption |
Summary
- Build tables from
table,tr,thandtd, one row at a time. - Use
thfor every header cell, in the top row, the first column or both. - Group rows with
thead,tbodyandtfoot, and title the table withcaption. - Style tables with CSS, not obsolete attributes.
- Use tables only for tabular data, never for page layout.