Building Tables in HTML
Learn HTML table structure from scratch: understand the table, tr, td, and th tags, when to use tabular data properly, and why using tables for page layout is a mistake to avoid.
If you’ve been following this series from the start, you’ve built a real foundation. We opened with the basics in Introduction to HTML, worked through semantic structure, text formatting, links, images, and finished up Batch 5 with lists of all kinds. Every batch has added a new layer to your toolkit.
Now we’re opening Batch 6, and this batch is entirely about one subject: tables. HTML tables are one of those topics where the technical side is actually pretty simple. Four main tags, a clear nesting rule, and you’re mostly there. What takes a bit more thought is understanding when to use them and, just as importantly, when not to.
Tables have a complicated history on the web. For years, developers used them to lay out entire pages, and the habit left a lasting mess in a lot of codebases. We’re going to address that directly in this article. Understanding the mistake is half the lesson.
This is your complete html tables tutorial: the structure, the right use cases, the wrong ones, and everything you need to organize data in a way that’s clean, accessible, and meaningful.
01What Is an HTML Table, Actually?
An HTML table is an element designed for one specific purpose: displaying data that has a natural row-and-column relationship. Think of a spreadsheet. Each row is a record. Each column is a consistent attribute of that record. Together, they form a grid where you can read across a row to learn about one item, or scan down a column to compare the same attribute across multiple items.
That’s the definition of html tabular data: information that genuinely belongs in a grid. A sports league standings table. A pricing comparison chart. A weekly class timetable. A quarterly revenue report. These all have that natural structure.
What a table is not: a visual positioning tool. If you’re using a <table> to put two divs side by side on a page, you’re misusing it. We’ll break down exactly why later in this article.
The right mental model is: if your data would make sense in a spreadsheet, it probably makes sense in a table. If you’re just trying to make something look like a grid visually, that’s a CSS job.
02The Four Core Tags of HTML Table Structure
Every HTML table is built from four fundamental tags. Before you write a single line of actual table code, you need to understand what each one does and how they relate to each other.
<table> is the outer container. It wraps the entire table. Nothing else works without it.
<tr> stands for “table row.” Every horizontal row in your table lives inside this tag, whether it’s a header row or a row of actual data.
<td> stands for “table data.” This is your standard content cell. It goes inside a <tr> and holds the actual value: a name, a number, a date, whatever your data is.
<th> stands for “table header.” It sits in exactly the same position as a <td>, but it signals that this cell is a header for a column or row, not just another data point. This distinction carries real meaning for browsers, screen readers, and search engines.
Here’s what their nesting relationship looks like visually:
Name
Role
Country
Alice
Developer
USA
Carlos
Designer
Spain
<table> : wraps everything
<tr> : each row
<th> : header cell
<td> : data cell
The nesting order is fixed: <table> contains <tr> elements. Each <tr> contains <th> or <td> cells. That’s the structure, and it never changes. This is the entire backbone of every html table structure you’ll build.
03Your First HTML Table: Writing It Step by Step
Let’s start with the most minimal version possible, just to see the structure in action:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My First Table</title>
</head>
<body>
<table>
<tr>
<td>Alice</td>
<td>Developer</td>
<td>USA</td>
</tr>
<tr>
<td>Carlos</td>
<td>Designer</td>
<td>Spain</td>
</tr>
</table>
</body>
</html>
Try it. It works. You’ll see two rows of data sit in a simple grid. But there’s a problem: without column labels, the reader has no idea what “Alice,” “Developer,” and “USA” mean in relation to each other. Are those a name, a product category, and a city? Are they three different names? The data is there, but the context isn’t.
Now let’s fix that by adding a proper header row with <th>:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Table with Headers</title>
</head>
<body>
<table>
<tr>
<th>Name</th>
<th>Role</th>
<th>Country</th>
</tr>
<tr>
<td>Alice</td>
<td>Developer</td>
<td>USA</td>
</tr>
<tr>
<td>Carlos</td>
<td>Designer</td>
<td>Spain</td>
</tr>
</table>
</body>
</html>
Now the table makes sense. The first row uses <th> to clearly label each column. Browsers render <th> cells bold and centered by default, which is a visual cue. But the more important difference is semantic, and that’s what we’ll dig into next.
04The th Element: Why Headers Carry Real Meaning
Using <th> isn’t just about making text bold. It’s about communicating structure. This is the same principle we talked about in our piece on Semantic HTML: the right element for the right job isn’t just good practice, it’s what gives your HTML meaning beyond its visual appearance.
When a screen reader encounters a table, it uses <th> cells to provide context as it reads out data. Instead of just announcing “Alice, Developer, USA,” it can say “Name: Alice, Role: Developer, Country: USA.” That’s a completely different experience for someone using assistive technology, and it’s only possible because you used the right tag.
Search engines also read table headers. They help establish what each column of data represents, which feeds into how your page is understood and indexed.
Headers don’t only go on columns. You can use <th> for row headers as well. Here’s a timetable where both directions have headers:
<table>
<tr>
<th></th>
<th>9:00 AM</th>
<th>10:00 AM</th>
<th>11:00 AM</th>
</tr>
<tr>
<th>Monday</th>
<td>Math</td>
<td>English</td>
<td>Science</td>
</tr>
<tr>
<th>Tuesday</th>
<td>History</td>
<td>Art</td>
<td>P.E.</td>
</tr>
</table>
Here the column headers are the time slots (9:00 AM, 10:00 AM, 11:00 AM), and the row headers are the days (Monday, Tuesday). Notice the top-left cell is an empty <th>. That’s intentional. It holds the space in the corner of the grid where the row and column headers intersect, and keeping it empty is the standard approach for this layout.
There’s also a scope attribute that makes this even more precise for accessibility. You use it like this:
<!-- Applies to the whole column below it -->
<th scope="col">Name</th>
<!-- Applies to the whole row to its right -->
<th scope="row">Monday</th>
The scope attribute tells screen readers exactly which cells a header applies to. We’ll cover it in full detail in Article 3 of this batch alongside other accessibility-focused attributes. For now, just know it exists and that it’s a good habit to start forming.
05How the HTML Table Structure Actually Renders
Here’s something worth understanding before you build more complex tables: your HTML source reads top to bottom, row by row. But the browser renders it as a two-dimensional grid. You write rows, and the browser infers columns.
The first <td> or <th> in every row forms the first column. The second cell forms the second column. The browser lines them all up automatically. You never create a “column” element explicitly in HTML. Columns are implied by consistent cell positioning across rows.
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alice</td>
<td>98</td>
</tr>
<tr>
<td>Carlos</td>
<td>85</td>
</tr>
</table>
| Name | Score |
|---|---|
| Alice | 98 |
| Carlos | 85 |
There’s one important rule that comes out of this: every row should have the same number of cells. If row one has three cells and a data row has two, the table will misalign. The exception is when you intentionally span cells using colspan (spanning across columns) or rowspan (spanning across rows). We’ll cover both in Article 3. For now, keep it consistent.
06When HTML Tabular Data Makes a Table the Right Choice
Here’s the core question to ask every time: “Does my content have a natural row-and-column relationship where each row is a record and each column is a consistent attribute of that record?”
If yes, use a table. If not, don’t. It’s genuinely that simple.
Here are the situations where tables are exactly the right tool:
Sports Standings
Teams as rows, stats like wins, losses, and points as columns. The relationship is inherently tabular.
Pricing Comparison
Features as rows, pricing tiers as columns. Readers scan both directions to decide what to buy.
Class Timetable
Days as rows, time slots as columns. Each cell is a subject at a specific day and time.
Financial Reports
Periods as rows, metrics like revenue, expenses, and profit as columns. Numbers with context.
Product Specs
Products as rows, attributes like weight, price, and color as columns. Easy to compare.
Data Records
Database results or exported CSV data. Rows and columns map to the data structure naturally.
The pattern you’ll notice in every case above: scanning across a row gives you a complete picture of one record. Scanning down a column lets you compare that one attribute across all records. Both directions carry meaning. That’s what makes something genuinely tabular data.
07Tables for Page Layout: The One Practice to Completely Avoid
Let’s address the complicated history directly, because it still shows up in codebases and tutorials, and you might encounter it.
Before CSS layout tools were reliable or widely supported, developers used <table> elements to position content on pages. A header in one row spanning all columns. A navigation sidebar in one cell. Main content in the next cell. A footer in another row. The entire page structure was built from nested tables. Some developers even used 1×1 pixel transparent GIF images as spacers inside table cells to force exact positioning. It was creative, but it was the wrong tool for the job.
That era is long over. Here’s why using tables for layout is a problem today:
It lies to the browser. A <table> element communicates one specific thing: “this is data with a row-column relationship.” When you use it to position your navigation next to your main content, you’re sending false signals to every browser, screen reader, and search engine that reads your page. Everything we’ve learned about semantic HTML in this series points to this being exactly the wrong approach.
It breaks accessibility. Screen readers navigate tables by announcing rows and columns. A sighted user skimming the page doesn’t notice that it’s built with tables, but someone relying on a screen reader hears the layout structure announced as data cells. Navigation links become “Row 1, Column 1: Home” type announcements. It’s confusing, disorienting, and fails accessibility standards.
It kills responsiveness. Tables are rigid. They don’t reflow for small screens. A page layout built entirely with tables will overflow horizontally on mobile and become essentially unusable. CSS layout tools (which we’ll cover thoroughly in the CSS series) are built from the ground up to handle responsive design. Tables are not.
It bloats the HTML. Nested layout tables mean deeply nested, repetitive markup. Pages that should have 100 lines of HTML end up with 500. Harder to read, harder to maintain, and slower to parse.
Using <table> purely to put columns side by side on a page. There is no actual data relationship. Screen readers announce these as table cells, not page regions. Breaks on mobile. Bad semantics.
| Plan | Storage | Price |
|---|---|---|
| Basic | 10 GB | $5/mo |
| Pro | 100 GB | $15/mo |
| Max | Unlimited | $49/mo |
Each row is a plan. Each column is an attribute of that plan. Scanning across gives you everything about one plan. Scanning down compares one attribute across all plans. Genuine tabular data.
The rule is clean: use <table> when you have data. Use CSS when you need layout. CSS layout (Flexbox and Grid in particular) will be covered fully in its own series. For now, whenever you feel the urge to use a table for positioning, recognize it as a CSS problem, not a table problem.
08Organizing HTML Tabular Data Properly
Knowing when to use a table is one thing. Knowing how to structure the data inside it well is another. Here’s what good organization looks like:
Label every column and row that carries its own meaning. If a column lists revenue figures, give it a header that says “Revenue.” If rows represent specific products, give each row a header with the product name. Never leave your reader guessing what a column or row represents. This is how you build a proper html semantic table.
Keep cell counts consistent across rows. Every row in your data section should have the same number of cells. If one row has four cells and another has three, the table will misalign. The only exception is intentional cell merging with colspan or rowspan, which we’ll cover in Article 3.
Order your data logically. Alphabetically for names. Chronologically for dates. Highest to lowest for rankings. The order should help the reader navigate the data, not create extra work for them.
Keep cell content brief. A table cell is designed to hold a value: a name, a number, a short label, a checkmark. If you’re writing sentences inside cells, the data probably doesn’t belong in a table format. Tables are for comparison and reference, not narrative.
Watch your column count. A table with seven or more columns becomes very hard to scan, especially on smaller screens. If you’re adding that many columns, consider whether some information can be shown differently or broken into a separate, more focused table.
09A Real-World HTML Tables Tutorial: Full Styled Example
Let’s build something realistic. Here’s a developer tools comparison table with proper structure and basic CSS styling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Developer Tools Comparison</title>
<style>
table {
width: 100%;
border-collapse: collapse;
font-family: sans-serif;
font-size: 15px;
}
th {
background-color: #6366f1;
color: white;
padding: 10px 16px;
text-align: left;
}
td {
padding: 10px 16px;
border-bottom: 1px solid #e2e8f0;
color: #334155;
}
tr:nth-child(even) td {
background-color: #f8fafc;
}
tr:hover td {
background-color: #ede9fe;
}
</style>
</head>
<body>
<table>
<tr>
<th>Tool</th>
<th>Category</th>
<th>Free Tier</th>
<th>Best For</th>
</tr>
<tr>
<td>VS Code</td>
<td>Code Editor</td>
<td>Yes</td>
<td>All-purpose development</td>
</tr>
<tr>
<td>GitHub</td>
<td>Version Control</td>
<td>Yes</td>
<td>Code hosting and collaboration</td>
</tr>
<tr>
<td>Figma</td>
<td>Design Tool</td>
<td>Yes</td>
<td>UI/UX design and prototyping</td>
</tr>
<tr>
<td>Postman</td>
<td>API Testing</td>
<td>Yes</td>
<td>Testing REST APIs</td>
</tr>
<tr>
<td>Vercel</td>
<td>Deployment</td>
<td>Yes</td>
<td>Deploying frontend projects</td>
</tr>
</table>
</body>
</html>
A quick note on the CSS here, since this article is about HTML: CSS will be covered in its own series on this site, but tables are one of those topics where you’d be left with a pretty rough result without at least a little styling context. The properties you see in this example are standard starting points for any table.
border-collapse: collapse is the one you’ll use every single time. By default, tables have double borders between cells because both the <table> element and each individual cell have their own border. collapse merges them into clean single lines. Leave it out and your table will look broken. That’s all you need to know for now.
Here’s what a properly styled table looks like in practice, one you might see on a real product page:
| Feature | Starter | Pro | Business |
|---|---|---|---|
| Projects | 3 | Unlimited | Unlimited |
| Storage | 500 MB | 50 GB | 500 GB |
| Team Members | 1 | 10 | Unlimited |
| Custom Domain | ✕ | ✓ | ✓ |
| Analytics Dashboard | ✕ | ✓ | ✓ |
| Priority Support | ✕ | ✕ | ✓ |
| Monthly Price | Free | $12 | $49 |
The HTML behind this table is the same structure you just learned. The visual polish is entirely CSS. You’ll also notice <thead> and <tbody> wrapping the sections, which is the next article in this batch.
You’ll notice this demo uses <thead> and <tbody> tags. These are semantic grouping elements that separate the header section from the data section of a table. They’re optional for the table to function, but they’re part of writing a complete, well-structured html semantic table, and they give you much more CSS control. They’re the primary topic of Article 2 in this batch, “What Are Table Headers, Body, and Footer Elements,” so we’ll give them the full treatment there.
10What This Batch Covers From Here
This is the foundation. Here’s what the rest of Batch 6 builds on top of it:
Article 2 goes into <thead>, <tbody>, and <tfoot>. These elements group the sections of your table semantically and make both styling and accessibility significantly better. They’re not optional extras for real-world tables.
Article 3 covers colspan and rowspan, the attributes that let you merge cells across multiple columns or rows. That’s how you build tables with headers that span multiple columns, or cells that cover multiple rows in complex data layouts. Also covers the scope attribute in full.
Article 4 covers table captions using the <caption> element, for adding context and descriptions to your tables that help both accessibility tools and readers understand what the table is about before they read it.
Article 5 covers responsive tables: how to make your tables work well on small screens. Tables are one of the trickier elements to make mobile-friendly, and there are several solid strategies for handling it.
11Common Mistakes Worth Knowing Now
Using tables for page layout. We covered this in detail. Don’t do it. Use CSS when you need to position elements.
Skipping <th> entirely. A table without header cells is like a spreadsheet with no column labels. It works visually but fails semantically. Always mark your headers, both for columns and for rows when applicable.
Mismatched cell counts across rows. Every row in the data section should have the same number of cells unless you’re intentionally using colspan or rowspan. Inconsistent counts break column alignment.
Forgetting border-collapse: collapse. By default, tables render with double borders between cells. This is almost never what you want. Add it to your table’s CSS as a habit.
Putting long blocks of text inside cells. Table cells aren’t content containers for paragraphs. If you’re tempted to write sentences inside a <td>, that data probably doesn’t belong in a table at all.
Building tables with too many columns. Six or more columns are hard to scan on any screen size and become nearly unreadable on mobile. Keep tables focused. If you have a lot of attributes, consider whether they can be split or presented in a different format. Responsive table techniques are coming in Article 5.
12Quick Reference: HTML Table Structure Cheat Sheet
The outer wrapper for the entire table. Required. Everything lives inside this element.
Table row. Creates a horizontal row. Lives inside <table> directly, or inside <thead>, <tbody>, or <tfoot>.
Table data cell. Holds actual content. Lives inside a <tr>. The standard cell type for data rows.
Table header cell. Semantic label for a column or row. Bold and centered by default. Lives inside a <tr>.
Attribute on <th>. Tells screen readers this header describes the full column below it.
Attribute on <th>. Tells screen readers this header describes the full row to its right.
CSS property applied to <table>. Merges default double borders into clean single lines.
Semantic grouping elements for table sections. Improve styling control and accessibility. Covered in Article 2.
Attributes on <td> or <th> for merging cells across columns or rows. Covered in Article 3.
13What You Learned Today
You now have the full foundation of HTML tables. You know the four core tags and how they nest. You know that <th> isn’t just bold text but a semantic signal that carries real meaning for accessibility and SEO. You know what genuine html tabular data looks like and how to recognize when a table is the right tool.
And just as importantly, you know when to walk away from tables entirely, specifically when the goal is layout rather than data representation. That distinction will save you from real problems as your pages grow more complex.
The MDN Web Docs have a great extended reference on the HTML table element if you want to explore further on your own. But everything you need to build real, accessible, well-structured tables is right here in this batch.
Next up: <thead>, <tbody>, and <tfoot>. Those three elements take your tables from basic to properly structured, and they’re what separates beginner-level HTML from the kind of clean, professional markup that holds up in real projects.
Building Tables in HTML
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.