Table Headers, Body, and Footer Elements in HTML
Learn to structure HTML tables using thead, tbody, and tfoot. Improve your site’s accessibility and SEO with proper semantic markup for professional data tables.
In the previous article on Building Tables in HTML, we covered the four core tags: <table>, <tr>, <th>, and <td>. That’s enough to get a working table on your screen.
But here’s the honest truth: just because it works doesn’t mean it’s structured properly.
When you dump all your rows inside a <table> without any grouping, the browser treats every single row the same. There’s no clear boundary between where your header ends, where your data begins, and where a summary row at the bottom belongs. It’s all flat. And flat tables create real problems for styling, accessibility, and code readability.
That’s exactly the gap that <thead>, <tbody>, and <tfoot> fill. These three elements give your table a proper skeleton, the same way <header>, <main>, and <footer> give your page structure. If you’ve read the article on What Is Semantic HTML and Why It’s Your Secret Weapon, you already know how much meaningful structure matters in HTML. The same principle applies here, inside your tables.
Let’s go through each element one by one, and by the end of this article, you’ll know how to build professional, accessible, and well-structured data tables from scratch.
01A Visual Look at the Three Sections
Before we dive into the code, let me show you what these three elements actually represent in a table. This visual will give you a mental model to hold onto as we go through each one.
Header section, contains column labels
| Product | Category | Qty | Price |
|---|
Body section, contains actual data rows
| Keyboard | Hardware | 3 | $45.00 |
| Mouse | Hardware | 5 | $22.00 |
| Monitor | Display | 2 | $230.00 |
Footer section, contains totals or summaries
| Total Items: 10 | 10 | $597.00 | |
Clear, right? Now let’s write some actual code.
02The thead Tag in HTML
The <thead> element wraps all the header rows of your table. Usually that’s just one row, containing the column labels that tell users what each column represents.
Think of it like the top section of a spreadsheet where the column names live (Name, Age, Score, and so on). That’s what goes inside <thead>.
Here’s the basic syntax:
<table>
<thead>
<tr>
<th>Name</th>
<th>Subject</th>
<th>Score</th>
</tr>
</thead>
</table>
A couple of things to keep in mind here.
First, <thead> wraps <tr> elements, and those rows contain <th> cells (header cells). You can technically use <td> inside thead, but that defeats the purpose. Use <th> for headers so the browser and screen readers know these are column labels, not data.
Second, a table can only have one <thead>. That’s it. One header section per table.
The html thead tag also plays a specific role when your table is printed or when a table spans multiple printed pages. The browser will repeat the header row on each page so the reader always knows what the columns mean. That’s a surprisingly useful behavior that costs you nothing to get.
03The tbody Tag in HTML
The <tbody> element wraps all the actual data rows in your table. Everything that isn’t a header or a footer goes here.
<table>
<thead>
<tr>
<th>Name</th>
<th>Subject</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Math</td>
<td>92</td>
</tr>
<tr>
<td>Bob</td>
<td>Science</td>
<td>87</td>
</tr>
<tr>
<td>Carol</td>
<td>English</td>
<td>95</td>
</tr>
</tbody>
</table>
One thing about <tbody> that catches a lot of beginners off guard: unlike <thead> and <tfoot>, you’re actually allowed to have multiple <tbody> elements in a single table. This is useful for grouping related rows, and we’ll look at a practical example of that later in this article.
The html tbody tag is also the element browsers target when they render striped row styles (alternating row colors). If you’ve ever written CSS like tbody tr:nth-child(odd), now you know why that selector works so well.
Something worth knowing about browsers and <tbody>
If you write a table with rows directly inside <table> and forget to add <tbody>, the browser will silently insert it for you in the DOM. So even if your HTML doesn’t have it, your rendered page will. This is fine, but it means if you write CSS targeting tbody, it will still match even when you didn’t write <tbody> in your code. That said, you should still write it explicitly. Relying on browser magic is not a good habit, and your code will be much more readable and maintainable when the structure is visible in your HTML.
04The tfoot Tag in HTML
The <tfoot> element wraps the footer rows of your table. This is typically where you’d put totals, averages, counts, or any kind of summary information that wraps up the data above it.
Honestly, when I first came across <tfoot>, I thought it was rarely used and mostly optional. But once you start building real data tables (invoices, reports, dashboards, financial summaries), you realize it has a very clear and important job.
Here’s the basic syntax:
<table>
<thead>
<tr>
<th>Name</th>
<th>Subject</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Math</td>
<td>92</td>
</tr>
<tr>
<td>Bob</td>
<td>Science</td>
<td>87</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">Average Score</td>
<td>89.5</td>
</tr>
</tfoot>
</table>
The colspan="2" in the footer row merges the first two cells into one. We’ll cover colspan and rowspan in detail in the next article. For now, just know it’s used here to span the label across two columns so it lines up correctly.
Where does <tfoot> go in your HTML?
In HTML5, you can place <tfoot> either before or after <tbody> in your source code. The browser will always render it at the bottom of the table regardless of where you write it. Most developers write it after <tbody> because that order feels natural and matches the visual output. Both are valid. Pick one style and stay consistent.
05Putting All Three Together: A Structured Table
Now let’s write a complete table using all three elements. Here’s a student grades table that uses <thead>, <tbody>, and <tfoot> properly:
<table>
<thead>
<tr>
<th>Student</th>
<th>Subject</th>
<th>Grade</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice Johnson</td>
<td>Mathematics</td>
<td>A</td>
<td>94</td>
</tr>
<tr>
<td>Bob Martinez</td>
<td>Science</td>
<td>B+</td>
<td>88</td>
</tr>
<tr>
<td>Carol White</td>
<td>English</td>
<td>A-</td>
<td>91</td>
</tr>
<tr>
<td>Dan Lee</td>
<td>History</td>
<td>B</td>
<td>84</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Class Average</td>
<td>89.3</td>
</tr>
</tfoot>
</table>
And here’s what a properly styled version of that looks like:
Student Grades Table
A complete table using <thead>, <tbody>, and <tfoot>
tbody: Data Rows
tfoot: Summary Row
| Student | Subject | Grade | Score |
|---|---|---|---|
| Alice Johnson | Mathematics | A | 94 |
| Bob Martinez | Science | B+ | 88 |
| Carol White | English | A- | 91 |
| Dan Lee | History | B | 84 |
| Class Average | 89.3 | ||
See how each section visually separates itself? The header is purple, the body rows are dark and clean, and the footer has a distinct pink tint. That separation comes entirely from targeting each section with CSS. Let’s look at how to do that.
06Styling Each Section Independently with CSS
One of the best practical benefits of using <thead>, <tbody>, and <tfoot> is that you can style each section completely independently. Instead of targeting specific row numbers or adding classes to individual rows, you just target the section element.
Here’s a clean CSS example you can build on:
<style>
table {
width: 100%;
border-collapse: collapse;
}
/* Style the header section */
thead th {
background-color: #6366f1;
color: #ffffff;
padding: 12px 16px;
text-align: left;
font-weight: 600;
}
/* Style the body rows */
tbody td {
padding: 11px 16px;
border-bottom: 1px solid #e2e8f0;
color: #334155;
}
/* Subtle zebra striping on body rows */
tbody tr:nth-child(even) td {
background-color: #f8fafc;
}
/* Style the footer section */
tfoot td {
padding: 12px 16px;
background-color: #f1f5f9;
font-weight: 700;
border-top: 2px solid #6366f1;
color: #1e293b;
}
</style>
<table>
<thead>
<tr>
<th>Student</th>
<th>Subject</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>Mathematics</td>
<td>94</td>
</tr>
<tr>
<td>Bob</td>
<td>Science</td>
<td>88</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">Average Score</td>
<td>91</td>
</tr>
</tfoot>
</table>
Notice how clean this is. The selectors thead th, tbody td, and tfoot td are simple and precise. No messy class names, no row index tricks. The semantic structure does the heavy lifting for you.
Sticky Headers with CSS: A Bonus Trick
Another thing you get for free when you use <thead>: sticky headers inside scrollable containers. If you have a long table inside a div with a fixed height, you can make the header stick to the top using just a few lines of CSS. The same trick works with <tfoot> for a sticky footer row.
<style>
.table-container {
max-height: 250px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: #6366f1;
color: #ffffff;
padding: 10px 14px;
}
tbody td {
padding: 10px 14px;
border-bottom: 1px solid #e2e8f0;
}
tfoot td {
position: sticky;
bottom: 0;
background-color: #f8fafc;
font-weight: 700;
padding: 10px 14px;
border-top: 2px solid #6366f1;
}
</style>
<div class="table-container">
<table>
<thead>
<tr>
<th>Month</th>
<th>Revenue</th>
<th>Orders</th>
</tr>
</thead>
<tbody>
<!-- many rows here -->
</tbody>
<tfoot>
<tr>
<td>Total</td>
<td>$48,200</td>
<td>430</td>
</tr>
</tfoot>
</table>
</div>
Here’s what that sticky behavior looks like in practice. Scroll down in this table and watch what happens to the header and footer rows:
Sticky thead and tfoot in action
Scroll inside the table below, the header and footer rows stay fixed.
| Month | Revenue | Orders |
|---|---|---|
| January | $3,200 | 28 |
| February | $4,100 | 35 |
| March | $3,800 | 31 |
| April | $5,600 | 47 |
| May | $6,200 | 54 |
| June | $4,900 | 42 |
| July | $5,100 | 44 |
| August | $4,500 | 39 |
| September | $3,700 | 32 |
| Total | $41,100 | 352 |
That’s the header staying pinned at the top, and the footer staying pinned at the bottom, as you scroll through the data. This works because position: sticky on thead th and tfoot td is anchored to their respective section boundaries. You could never pull this off cleanly without the semantic structure underneath.
Note: We’re using CSS here, which we’ll cover in its own series later. But I wanted to show you a practical reason why this structure matters now, before you even get to CSS.
07Why This Structure Matters for Accessibility
Screen readers and assistive technologies don’t just read your content top to bottom. They understand the semantics of your HTML and use it to give users context. When you use <thead>, <tbody>, and <tfoot> correctly, screen reader users get:
- A clear announcement when the header section begins
- The ability to navigate directly to the data rows in
<tbody> - A distinct announcement for the footer summary section
Without these structural elements, a screen reader would just announce “table row 1”, “table row 2”, and so on, with no semantic difference between a header row and a data row. That creates a confusing experience for someone navigating the table.
We covered how semantic structure directly improves accessibility in the article on Semantic HTML, and tables are a perfect example of that principle in action. A table with proper semantic structure is one that every user, regardless of how they access the web, can understand and navigate effectively.
Accessibility isn’t a feature you add later. It’s baked into the structure from the start. And using <thead>, <tbody>, and <tfoot> is one of the simplest ways to make that happen.
08Using Multiple tbody Elements to Group Your Data
Here’s something most people don’t know: you can use more than one <tbody> in the same table. The <thead> and <tfoot> can only appear once each, but <tbody> can repeat as many times as you need.
This is useful when your data has natural groups. Say you’re showing sales data split by region, or products split by category. Instead of adding classes or visual dividers manually, you can group rows into separate <tbody> elements and style them differently.
<table>
<thead>
<tr>
<th>Product</th>
<th>Category</th>
<th>Units Sold</th>
</tr>
</thead>
<!-- First group: Hardware -->
<tbody>
<tr>
<td colspan="3">Hardware</td>
</tr>
<tr>
<td>Mechanical Keyboard</td>
<td>Input</td>
<td>142</td>
</tr>
<tr>
<td>Gaming Mouse</td>
<td>Input</td>
<td>198</td>
</tr>
</tbody>
<!-- Second group: Software -->
<tbody>
<tr>
<td colspan="3">Software</td>
</tr>
<tr>
<td>License Pro</td>
<td>Subscription</td>
<td>310</td>
</tr>
<tr>
<td>Plugin Bundle</td>
<td>Add-on</td>
<td>87</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">Total Units Sold</td>
<td>737</td>
</tr>
</tfoot>
</table>
And here’s how that looks visually when each <tbody> is styled as a distinct group:
Multiple tbody Groups in One Table
Each product category is wrapped in its own <tbody> for independent styling.
| Product | Category | Units Sold |
|---|---|---|
| Hardware | ||
| Mechanical Keyboard | Input | 142 |
| Gaming Mouse | Input | 198 |
| Software | ||
| License Pro | Subscription | 310 |
| Plugin Bundle | Add-on | 87 |
| Total Units Sold | 737 | |
Each group is visually distinct without adding a single extra class to the data rows. The separation is semantic, which means the structure carries meaning beyond just the visual appearance.
09A Complete Real-World Example: Project Invoice Table
Let’s put everything together in a real-world scenario. This is the kind of table you’d actually build for a client invoice, a financial report, or a project summary page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Project Invoice</title>
<style>
body {
font-family: sans-serif;
padding: 24px;
background: #f8fafc;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: #0f172a;
color: #94a3b8;
padding: 10px 14px;
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.07em;
}
thead th:last-child { text-align: right; }
tbody td {
padding: 14px;
color: #334155;
border-bottom: 1px solid #e2e8f0;
vertical-align: top;
}
tbody td:last-child {
text-align: right;
font-weight: 500;
color: #0f172a;
}
tfoot td {
padding: 11px 14px;
color: #64748b;
border-top: 1px solid #e2e8f0;
text-align: right;
}
tfoot td:first-child,
tfoot td:nth-child(2),
tfoot td:nth-child(3) { text-align: left; }
tfoot tr.grand-total td {
font-size: 16px;
font-weight: 700;
color: #6366f1;
border-top: 2px solid #6366f1;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Description</th>
<th>Type</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>Website Design</td>
<td>Design</td>
<td>1</td>
<td>$800.00</td>
<td>$800.00</td>
</tr>
<tr>
<td>Frontend Development</td>
<td>Development</td>
<td>20 hrs</td>
<td>$75.00</td>
<td>$1,500.00</td>
</tr>
<tr>
<td>SEO Setup</td>
<td>Marketing</td>
<td>1</td>
<td>$250.00</td>
<td>$250.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">Subtotal</td>
<td>$2,550.00</td>
</tr>
<tr>
<td colspan="4">Tax (8%)</td>
<td>$204.00</td>
</tr>
<tr class="grand-total">
<td colspan="4">Total Due</td>
<td>$2,754.00</td>
</tr>
</tfoot>
</table>
</body>
</html>
Notice how the <tfoot> contains three rows: subtotal, tax, and the grand total. The footer section isn’t limited to one row. It can have as many rows as you need to present the summary information clearly.
Here’s that invoice table rendered with the brand styling:
Project Invoice #INV-2026-04
Paid
| Description | Type | Qty | Amount |
|---|---|---|---|
| Website Design | Design | 1 | $800.00 |
| Frontend Development | Development | 20 hrs | $1,500.00 |
| SEO Setup | Marketing | 1 | $250.00 |
| Subtotal | $2,550.00 | ||
| Tax (8%) | $204.00 | ||
| Total Due | $2,754.00 | ||
This is exactly the kind of table you’d build in a real project. The structure is clean, the sections are meaningful, and styling each part is straightforward because the HTML tells you what each section is.
10Common Mistakes to Avoid
A few things I see beginners get wrong with these elements:
Putting Header Cells Inside tbody
Sometimes people write their header row inside <tbody> instead of <thead> because they’re not sure where it goes. The table will still display, but you lose the semantic benefit entirely. Column header rows should always be in <thead>.
Skipping tbody Entirely
Some developers write rows directly inside <table>, skipping <tbody> because the browser silently adds it anyway. While this technically works, it’s a bad habit. Your code becomes harder to read, and you can’t independently style the body section or add multiple body groups later without refactoring your markup.
Using tfoot as a Visual Styling Trick
The <tfoot> element has semantic meaning. It’s for summary or footer content that summarizes the data above it. Don’t use it just to make a row look different visually. If you want a visually distinct row that isn’t a semantic summary, just add a CSS class to that row instead.
Nesting Tables Inside These Sections
If you need to nest a table inside another table (which is rare and usually the wrong approach), the nested table should have its own complete <thead>, <tbody>, and <tfoot> structure. Mixing up the parent and child table sections leads to unpredictable rendering.
Don’t use tables for page layout
This is something we mentioned in the Building Tables in HTML article, and it’s worth repeating here: tables are for tabular data, not page layout. Even with a perfectly structured <thead>, <tbody>, and <tfoot>, using a table to create a two-column page layout is semantically wrong and causes real accessibility problems. Use CSS Grid or Flexbox for layout purposes. Tables are for data that belongs in rows and columns.
11Quick Reference: thead, tbody, and tfoot at a Glance
Quick Reference Cheat Sheet
- Wraps header rows
- Contains column label cells
- Use <th> elements inside
- Only one per table
- Repeated on print page breaks
- Can be made sticky with CSS
- Wraps all data rows
- Contains <td> cells (and <th scope=”row”>)
- Multiple allowed in one table
- Auto-inserted by browsers if omitted
- Target for zebra striping in CSS
- Best practice: always write it explicitly
- Wraps footer / summary rows
- Ideal for totals, averages, counts
- Only one per table
- Can appear before or after tbody in HTML5
- Renders at bottom regardless
- Can be made sticky with CSS
12What You Learned Today
You now know how to give your HTML tables a proper structure. That’s a bigger deal than it might seem.
You learned that the thead tag in HTML holds your header rows, that the tbody tag in HTML holds your data (and can be used multiple times for grouping), and that the tfoot tag in HTML holds your summary or footer rows. You also saw how each section can be styled independently, how sticky headers and footers work, and how this structure directly benefits accessibility.
Next up in this batch, we’re covering Colspan, Rowspan, and Scope, where we’ll learn how to merge cells across rows and columns and build more complex table layouts. That one builds directly on what you’ve learned today, so you’re already in a great spot heading into it.
For now, take the tables you’ve already built and add <thead>, <tbody>, and <tfoot> to them. It’s a small change, but it’s one that separates code written casually from code written with intention.
Table Headers, Body, and Footer Elements in HTML
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.