📊 Table Structure and Semantics
A table is more than a grid of boxes — it is a machine-readable statement about how pieces of data relate to one another. In this lesson you'll learn the semantic elements that give tables meaning, so browsers, search engines, and screen readers all understand your data the same way you do.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Build a table from its core parts —
<table>,<tr>,<th>, and<td> - Add semantic structure with
<caption>,<thead>,<tbody>, and<tfoot> - Distinguish column headers from row headers and mark both correctly
- Merge cells with
colspanandrowspanwhile keeping the grid valid - Decide when a table is the right tool — and when a list or layout is better
Estimated Time: 30–40 minutes • Difficulty: Beginner
Hands-on: Build a fully semantic quarterly-sales table with merged cells and a footer of totals.
In This Lesson
Why Tables Exist
An HTML table represents two-dimensional data: values that only make sense at the intersection of a row and a column. A price is meaningless until you know which product (the row) and which store (the column) it belongs to. Tables encode exactly that kind of relationship, and they do it in a way that assistive technology can read aloud without losing the meaning.
💡 A useful analogy: Think of a spreadsheet. Every filled cell carries an invisible label from the top of its column and the start of its row. A well-built HTML table keeps those labels attached to the data, so a screen reader can announce "Laptop, Price, $999" instead of a lonely, contextless "$999".
⚠️ Tables are for data, not layout
In the early web, developers abused <table> to position page elements. That era is over. Page layout is the job of CSS Grid and Flexbox. Reserve tables strictly for information that genuinely lives in rows and columns.
Here's the map of what a table is made of — we'll walk through each branch:
The Building Blocks
Every table is assembled from four elements. Learn these and you can build anything:
📖 Key Elements
<table> — the container that wraps the whole grid.
<tr> — a table row; each holds one or more cells.
<th> — a header cell that labels a row or column (bold and centered by default).
<td> — a data cell holding an actual value.
Here is a minimal product table:
<table>
<tr>
<th>Product</th>
<th>Price</th>
<th>Stock</th>
</tr>
<tr>
<td>Laptop</td>
<td>$999.99</td>
<td>15</td>
</tr>
<tr>
<td>Smartphone</td>
<td>$499.99</td>
<td>42</td>
</tr>
</table>
Read the markup out loud and the structure becomes obvious: the table contains rows, and each row contains cells. The first row uses <th> to name the columns; the rest use <td> for values. That grid maps directly onto a familiar layout:
<tr>) stack vertically; within each row, header cells (<th>) and data cells (<td>) sit side by side to form columns.Semantic Sections
A bare table works, but four extra elements turn it into a properly semantic table — one whose parts carry meaning for browsers and assistive tech.
Caption: the table's title
The <caption> must be the first child of the table. Screen readers announce it the moment the table is reached, giving instant context.
<table>
<caption>Product Inventory — June 2026</caption>
<!-- rows go here -->
</table>
thead, tbody, tfoot: logical groups
These three elements divide the table into a header group, a body of data, and a footer (often used for totals). Grouping this way improves styling, accessibility, and printing — browsers can repeat the header on every printed page.
<table>
<caption>Product Inventory — June 2026</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
<th scope="col">Stock</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Laptop</th>
<td>$999.99</td>
<td>15</td>
</tr>
<tr>
<th scope="row">Smartphone</th>
<td>$499.99</td>
<td>42</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total Stock</th>
<td>—</td>
<td>57</td>
</tr>
</tfoot>
</table>
💡 Source order tip
In modern HTML you can write <tfoot> either before or after <tbody> — the browser always renders it at the bottom. Writing it after <tbody> reads most naturally and is the common convention today.
The document tree that markup produces looks like this:
Row & Column Headers
Headers are what make a table readable. There are three common patterns.
Column headers (most common)
The top row labels each column. This is the default mental model of a table.
<tr>
<th scope="col">Name</th>
<th scope="col">Age</th>
<th scope="col">City</th>
</tr>
Row headers
When each row is one record, the first cell of the row often labels it. Mark it as a header with scope="row".
<tr>
<th scope="row">Laptop</th>
<td>$999.99</td>
<td>15</td>
</tr>
Both — the two-way grid
Schedules and comparison matrices use headers on both axes. The top-left cell sits at the intersection and is usually left empty.
| Monday | Tuesday | Wednesday | |
|---|---|---|---|
| Morning | Gym | Meeting | Dentist |
| Afternoon | Lunch | Project work | Presentation |
📖 What is scope?
The scope attribute tells assistive technology which cells a header governs: scope="col" for a whole column, scope="row" for a whole row. You'll go deep on this in the next lesson — for now, add it to every <th> as a habit.
Spanning Cells
Real data isn't always a perfect grid. Two attributes let a single cell stretch across neighbours.
colspan — stretch across columns
<tr>
<th colspan="3">Product Information</th>
</tr>
That one header now sits above all three columns as a group title.
rowspan — stretch down rows
<tr>
<td rowspan="2">Electronics</td>
<td>Laptop</td>
<td>$999.99</td>
</tr>
<tr>
<!-- no category cell here — the one above spans into this row -->
<td>Smartphone</td>
<td>$499.99</td>
</tr>
⚠️ Keep the grid balanced
When a cell spans, the rows it covers must contain fewer cells to compensate — the spanning cell already occupies those positions. Add cells that shouldn't be there and the table's shape breaks. Count columns per row carefully.
Combining both builds a complex header structure. Here is a quarterly-sales table rendered from real semantic markup:
| Department | 2026 Quarterly Sales | |||
|---|---|---|---|---|
| Q1 | Q2 | Q3 | Q4 | |
| Electronics | $10,000 | $12,500 | $14,000 | $15,500 |
| Clothing | $8,000 | $9,500 | $10,000 | $12,000 |
| Total | $18,000 | $22,000 | $24,000 | $27,500 |
When to Use a Table
The single most important decision is whether your content is tabular at all. Use this flow to choose:
both rows AND columns?} -->|Yes| B[Use a table] A -->|No| C{What shape is it?} C -->|A sequence of items| D[Ordered / unordered list] C -->|Name → value pairs| E[Definition list dl] C -->|Blocks of content| F[CSS Grid / Flexbox] C -->|Form fields| G[fieldset + labels]
| Content | Right tool |
|---|---|
| Price comparison across 4 products | ✅ Table |
| A navigation menu | ❌ Use a <nav> + list |
| Page layout (sidebar + main) | ❌ Use CSS Grid |
| Spec sheet of name/value pairs | ❌ Use a <dl> |
Hands-on Exercise
🏋️ Build a Semantic Sales Table
Objective: Produce a complete, semantic table that uses every element from this lesson.
Requirements:
- A
<caption>titling the table. <thead>,<tbody>, and<tfoot>sections.- Column headers with
scope="col"and row headers withscope="row". - At least one merged cell using
colspanorrowspan. - A
<tfoot>row showing column totals.
💡 Hint
Start with the <thead> row and count its columns. Every <tbody> and <tfoot> row must add up to that same column count — remembering that a colspan="2" cell counts as two.
✅ Sample solution
<table>
<caption>Regional Sales — Q1 2026</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Online</th>
<th scope="col">In-store</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>$12,000</td>
<td>$8,000</td>
</tr>
<tr>
<th scope="row">South</th>
<td>$9,500</td>
<td>$6,500</td>
</tr>
<tr>
<th scope="row">New markets</th>
<td colspan="2">Launching next quarter</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">Total</th>
<td>$21,500</td>
<td>$14,500</td>
</tr>
</tfoot>
</table>
Notice the "New markets" row: its single colspan="2" cell fills the two value columns, so the row still totals three columns.
🎯 Quick Quiz
Question 1: Which element must be the first child of a <table> and gives it a title?
Question 2: You add rowspan="2" to a cell. What must you do to the next row?
Question 3: Which is the correct use for an HTML table?
Best Practices
✅ Do
- Give every table a meaningful
<caption>. - Use
<th>for all headers and add ascope. - Group rows with
<thead>,<tbody>, and<tfoot>. - Style with CSS (
border-collapse, zebra striping) — never with deprecated attributes likebgcolor. - Wrap wide tables in a scroll container for small screens.
❌ Don't
- Don't use tables for page layout.
- Don't fake headers with bold
<td>cells — they carry no semantics. - Don't nest tables inside tables; it confuses screen readers badly.
- Don't rely on the
borderHTML attribute — it's presentational and outdated.
A small, modern CSS baseline makes any semantic table readable:
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 0.75rem;
text-align: left;
border: 1px solid var(--border-color, #ddd);
}
tbody tr:nth-child(even) {
background: rgba(0, 0, 0, 0.03);
}
caption {
caption-side: top;
font-weight: 700;
padding: 0.5rem;
}
Summary & Quiz
🎉 Key Takeaways
- A table encodes two-dimensional relationships — data that needs both a row and a column to have meaning.
- The core four are
<table>,<tr>,<th>, and<td>. <caption>,<thead>,<tbody>, and<tfoot>add the semantics that assistive tech relies on.colspanandrowspanmerge cells — just keep the column count per row consistent.- Tables are for data only; layout belongs to CSS.
📚 Further Reading
🚀 What's Next?
You can now build a structurally sound table. Next we'll make it truly inclusive — the Table Accessibility Features lesson digs into scope, the id/headers pairing, ARIA, and screen-reader testing.
🎉 Well done!
Semantic tables are a small skill with outsized impact on readability and accessibility. Let's make them bulletproof next.