HTML Tables Deep Dive
When to Use HTML Tables
Use <table> for tabular data — financial reports, schedules, comparison charts. Never use tables for page layout; CSS Grid and Flexbox handle layout better and don’t confuse assistive technology.
Complete Table Anatomy
<table>
<caption>2024 Quarterly Revenue (USD)</caption>
<colgroup>
<col span="1" style="background: #f8f9fa">
<col span="3">
</colgroup>
<thead>
<tr>
<th scope="col" id="quarter">Quarter</th>
<th scope="col" id="product-a">Product A</th>
<th scope="col" id="product-b">Product B</th>
<th scope="col" id="total">Total</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row" headers="quarter">Q1</th>
<td headers="quarter product-a">$10,000</td>
<td headers="quarter product-b">$8,500</td>
<td headers="quarter total">$18,500</td>
</tr>
<tr>
<th scope="row" headers="quarter">Q2</th>
<td headers="quarter product-a">$12,400</td>
<td headers="quarter product-b">$9,200</td>
<td headers="quarter total">$21,600</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row" headers="quarter">Annual</th>
<td headers="product-a">$22,400</td>
<td headers="product-b">$17,700</td>
<td headers="total"><strong>$40,100</strong></td>
</tr>
</tfoot>
</table>
Accessibility Requirements
Screen readers announce table dimensions and header associations. Follow these rules:
<caption>— first child of<table>; describes the table purpose<th scope="col|row">— identifies header directionheadersattribute — on<td>when headers aren’t simple (complex tables)- Don’t use
scopeon<td>— only on<th>
For very complex tables (nested headers), use explicit id on headers and headers on cells.
Column and Row Spanning
<thead>
<tr>
<th rowspan="2" scope="col">Region</th>
<th colspan="2" scope="colgroup">Sales</th>
</tr>
<tr>
<th scope="col">Online</th>
<th scope="col">Retail</th>
</tr>
</thead>
colspan spans columns; rowspan spans rows. Validate that every data cell has a logical header.
Sortable Data Tables (HTML + JS Pattern)
<table id="users">
<thead>
<tr>
<th scope="col"><button type="button" data-sort="name">Name ↕</button></th>
<th scope="col"><button type="button" data-sort="email">Email ↕</button></th>
</tr>
</thead>
<tbody>...</tbody>
</table>
Use <button> inside <th> for sort controls — buttons are keyboard accessible. Announce sort state with aria-sort="ascending|descending|none".
Responsive Table Patterns
Horizontal Scroll Wrapper
<div style="overflow-x: auto;">
<table>...</table>
</div>
Simplest approach — preserves table semantics.
Stacked Cards (Mobile)
@media (max-width: 600px) {
table, thead, tbody, tr, th, td { display: block; }
thead { position: absolute; left: -9999px; }
td::before {
content: attr(data-label);
font-weight: bold;
display: block;
}
}
<td data-label="Email">[email protected]</td>
Hide Non-Essential Columns
@media (max-width: 768px) {
.col-secondary { display: none; }
}
Styling Tables
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
tbody tr:hover { background: #f8f9fa; }
caption { caption-side: top; font-weight: bold; margin-bottom: 0.5rem; }
Use border-collapse: collapse for clean borders. Zebra striping: tbody tr:nth-child(even) { background: #f8f9fa; }.
<colgroup> and <col>
Style entire columns without classes on every cell:
<colgroup>
<col class="col-label">
<col span="3" class="col-data">
</colgroup>
.col-label { background: #e9ecef; width: 30%; }
Common Mistakes
| Mistake | Fix |
|---|---|
| Layout tables | Use CSS Grid |
Missing <caption> |
Add descriptive caption |
Header cells as <td> |
Use <th scope="..."> |
| Empty cells for spacing | CSS padding on <td> |
Troubleshooting
Screen reader reads wrong column header
- Add
scope="col"orscope="row"; useheadersfor complex tables
Table overflows on mobile
- Wrap in scroll container or implement stacked pattern
Sort breaks after DOM update
- Re-apply
aria-sort; maintain focus on sort button
Tables are powerful for structured data — build them accessibly first, then enhance with CSS and JavaScript.