HTML Tables
A <table> displays data in rows and columns, things like a price list, a schedule, or a comparison chart. Tables are for tabular data only. Don’t use a table to lay out a page, CSS Flexbox and Grid handle layout, and a layout table confuses screen readers trying to read it as data.
Creating a table
<table>wraps the whole table.<tr>(table row) holds one row.<td>(table data) holds one cell of data.<th>(table header) marks a cell as a header for a row or column, instead of<td>.
<table>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
<tr>
<td>Amina</td>
<td>Engineer</td>
</tr>
<tr>
<td>Priya</td>
<td>Designer</td>
</tr>
</table>
Caption
<caption> gives the table a title. It’s announced by screen readers right when they enter the table, which is more reliable than a heading placed above the table with no direct connection to it. Put it right after the opening <table> tag.
<table>
<caption>Team roles, 2026</caption>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
<tr>
<td>Amina</td>
<td>Engineer</td>
</tr>
</table>
Accessible headers with scope
The scope attribute on a <th> tells screen readers whether that header applies to a column or a row. This matters a lot on data-heavy tables, without it, a screen reader can only guess which header goes with a given cell.
<table>
<caption>Quarterly revenue</caption>
<tr>
<th scope="col">Quarter</th>
<th scope="col">Revenue</th>
</tr>
<tr>
<th scope="row">Q1</th>
<td>$12M</td>
</tr>
<tr>
<th scope="row">Q2</th>
<td>$16M</td>
</tr>
</table>
Table sections: thead, tbody, tfoot
Larger tables can be split into a head, a body, and a footer. This groups rows semantically and gives you clear hooks for CSS.
<table>
<caption>Quarterly revenue and costs</caption>
<thead>
<tr>
<th scope="col">Quarter</th>
<th scope="col">Revenue</th>
<th scope="col">Costs</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Q1</th>
<td>$12M</td>
<td>$10M</td>
</tr>
<tr>
<th scope="row">Q2</th>
<td>$16M</td>
<td>$2M</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td>$28M</td>
<td>$12M</td>
</tr>
</tfoot>
</table>
Spanning columns and rows
A cell can span multiple columns with colspan, or multiple rows with rowspan.
<table>
<caption>Weekly schedule</caption>
<tr>
<th scope="col">Monday</th>
<th scope="col">Tuesday</th>
<th scope="col">Wednesday</th>
</tr>
<tr>
<td colspan="2">Out of town</td>
<td>Back in town</td>
</tr>
</table>
Styling a table
Add borders and spacing with CSS, not the old HTML border attribute, which offers no control over colour, spacing, or style.
table {
border-collapse: collapse;
width: 100%;
}
th,
td {
border: 1px solid #ccc;
padding: 8px 12px;
text-align: left;
}
Common mistakes
- Using a table for page layout instead of actual data. Use CSS Grid or Flexbox for layout.
- Leaving out
<caption>andscope, which makes a table technically readable but hard to navigate with a screen reader. - Using
<td>for header cells instead of<th>, losing the semantic distinction between headers and data.
FAQ
Do I need thead/tbody/tfoot on every table?
No, they’re most useful on larger tables. A small two or three row table works fine with just <tr> rows directly inside <table>.
How do I make a table scroll on small screens instead of squishing?
Wrap the table in a container and let that container scroll horizontally:
.table-wrapper {
overflow-x: auto;
}
<div class="table-wrapper">
<table>...</table>
</div>
What to read next
- Forms : another common data-entry element with its own accessibility rules
- CSS Responsive Design : handling wide tables on small screens