Tables are built specifically for one job: displaying tabular data - information that naturally fits into rows and columns, like a price list, a schedule, or a comparison chart. In this post we are building real tables from scratch, covering headers, merged cells, and a few common real-world layouts. Let's jump into the code.
Quick Theory: The Core Table Tags
A basic table uses three tags: <table> as the container, <tr> for table row, and <td> for table data (an individual cell). There's also <th> for table header cells, which we'll get to shortly. Let's build one right away.
Practical: Your First Table
<table border="1">
<tr>
<td>Name</td>
<td>Age</td>
<td>City</td>
</tr>
<tr>
<td>Sara</td>
<td>28</td>
<td>Chicago</td>
</tr>
<tr>
<td>James</td>
<td>34</td>
<td>Boston</td>
</tr>
</table>Save this and open it in your browser. The border="1" attribute adds a visible border just so you can see the table's grid clearly while learning (in real projects, borders are usually handled with CSS instead). Notice each <tr> represents one full row, and each <td> inside it represents one cell in that row.
Practical: Adding Proper Header Cells with <th>
<table border="1">
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
<tr>
<td>Sara</td>
<td>28</td>
<td>Chicago</td>
</tr>
<tr>
<td>James</td>
<td>34</td>
<td>Boston</td>
</tr>
</table>Replace the top row's <td> tags with <th> and reload the page. You'll notice the header row automatically renders bold and centered by default. More importantly, using <th> tells screen readers and search engines that this row labels the columns below it, which is genuinely important information, not just a styling choice.
Practical: Structuring a Table with <thead>, <tbody>, and <tfoot>
<table border="1">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Notebook</td>
<td>$2.50</td>
<td>10</td>
</tr>
<tr>
<td>Pen</td>
<td>$0.75</td>
<td>20</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Total</td>
<td>$40.00</td>
<td>30</td>
</tr>
</tfoot>
</table>Try building this exact table. Visually it might look almost the same as before, but organizing your table into <thead>, <tbody>, and <tfoot> makes large tables far easier to style with CSS later (for example, giving the header a different background color) and easier for browsers to handle things like fixed headers on scrollable tables.
Practical: Adding a Table Caption
<table border="1">
<caption>Monthly Sales Report - March</caption>
<tr>
<th>Region</th>
<th>Sales</th>
</tr>
<tr>
<td>East</td>
<td>$12,000</td>
</tr>
<tr>
<td>West</td>
<td>$9,500</td>
</tr>
</table>Add a <caption> tag right after your opening <table> tag and test it - it displays a title directly above the table, which is the correct semantic way to label what a table represents, rather than putting a plain heading or paragraph above it.
Practical: Merging Cells with colspan
<table border="1">
<tr>
<th colspan="2">Contact Information</th>
</tr>
<tr>
<td>Email</td>
<td>sara@example.com</td>
</tr>
<tr>
<td>Phone</td>
<td>(555) 123-4567</td>
</tr>
</table>Try this - colspan="2" makes that header cell stretch across two columns instead of one, which is exactly why the second row only needs two cells to line up correctly underneath it. Test changing it to colspan="1" and notice how the layout breaks, since now there are three cells' worth of space but only two are filled.
Practical: Merging Cells with rowspan
<table border="1">
<tr>
<th>Day</th>
<th>Time</th>
<th>Activity</th>
</tr>
<tr>
<td rowspan="2">Monday</td>
<td>9:00 AM</td>
<td>Team Meeting</td>
</tr>
<tr>
<td>2:00 PM</td>
<td>Client Call</td>
</tr>
</table>Try this example - rowspan="2" makes the "Monday" cell stretch down across two rows, so it only needs to appear once instead of being repeated. This is a genuinely common pattern for schedules and timetables where one label applies to multiple rows underneath it.
Practical: Building a Pricing Comparison Table
Let's combine everything into something you'd actually see on a real website:
<table border="1">
<caption>Plan Comparison</caption>
<thead>
<tr>
<th>Feature</th>
<th>Basic</th>
<th>Pro</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$9/mo</td>
<td>$29/mo</td>
<td>$99/mo</td>
</tr>
<tr>
<td>Users</td>
<td>1</td>
<td>5</td>
<td>Unlimited</td>
</tr>
<tr>
<td>Storage</td>
<td>5GB</td>
<td>50GB</td>
<td>500GB</td>
</tr>
<tr>
<td>Support</td>
<td>Email</td>
<td>Email + Chat</td>
<td>24/7 Phone</td>
</tr>
</tbody>
</table>Build this exact table and open it in your browser. This is essentially how pricing pages across countless SaaS websites are structured under the hood, before any CSS styling is applied.
Practical: A Weekly Class Schedule Table
<table border="1">
<caption>Weekly Class Schedule</caption>
<tr>
<th>Time</th>
<th>Monday</th>
<th>Wednesday</th>
<th>Friday</th>
</tr>
<tr>
<td>9:00 AM</td>
<td>Math</td>
<td>Science</td>
<td>Math</td>
</tr>
<tr>
<td>11:00 AM</td>
<td>History</td>
<td>Art</td>
<td>History</td>
</tr>
</table>Try building this schedule too. Notice the very first column also acts like a header, labeling each row by time, while the top row labels each column by day. Real timetables you see in schools and universities follow exactly this kind of layout.
Mistake to Avoid: Using Tables for Page Layout
<!-- Don't do this -->
<table>
<tr>
<td>Sidebar content</td>
<td>Main page content</td>
</tr>
</table>Back in the early days of the web, developers commonly used tables to arrange entire page layouts - sidebars, headers, footers, all inside table cells. This is now considered outdated and incorrect practice. Tables should only be used for genuinely tabular data. Page layout today is handled with CSS, using tools like Flexbox or Grid, which we'll cover once we reach the CSS series.
Mistake to Avoid: Forgetting <th> for Headers
<!-- Less correct -->
<tr>
<td>Name</td>
<td>Age</td>
</tr>Using <td> for header labels works visually if you add bold styling manually, but it loses the semantic meaning that <th> provides for accessibility and screen readers. Always use <th> for cells that label rows or columns.
Mistake to Avoid: Mismatched Column Counts After Using colspan
<!-- This breaks the table structure -->
<tr>
<th colspan="3">Order Summary</th>
</tr>
<tr>
<td>Item</td>
<td>Price</td>
</tr>If your header spans 3 columns using colspan="3", every row below it needs to add up to 3 columns' worth of cells too. In this broken example, the second row only has 2 cells, which will misalign the entire table visually. Always double-check your column math whenever you use colspan or rowspan.
Your Turn: Practice Exercise
- Build a table listing five of your favorite movies, with columns for Title, Year, and Genre.
- Use
<th>for the header row and organize the table using<thead>and<tbody>. - Add a
<caption>describing the table. - Create a second table using
colspanto merge a header across multiple columns. - Create a third table using
rowspanto merge at least one cell across two rows. - Open all three in your browser and confirm the layouts render correctly.
Conclusion
Tables are purpose-built for tabular data, and once you understand rows, headers, and merged cells, you can build almost any real-world table layout - pricing charts, schedules, comparison grids, and reports. If you built the examples in this post, you now have hands-on experience with everything from basic tables to colspan and rowspan. In the next post, we'll move into "HTML Forms: Complete Beginner Guide", where we'll start collecting user input, which is a completely different skill set from anything we've covered so far.