Lesson 8 of 12

HTML Tables

Some information naturally has rows and columns: a train timetable, a price comparison, exam marks. HTML tables are designed for exactly this kind of tabular data. In this lesson you will build tables that are easy to read for everyone, including people using screen readers. If you have just finished HTML Lists, think of a table as a list where each item has several properties to compare.

The basic building blocks

A table is made from four core elements:

  • <table> wraps the whole table.
  • <tr> (table row) holds one row of cells.
  • <th> (table header) is a heading cell for a column or row.
  • <td> (table data) is an ordinary data cell.

HTML tables are built row by row. Each <tr> should contain the same number of cells (unless you merge cells, covered below), otherwise the columns will not line up.

Captions and header cells

The <caption> element gives the table a title. It must be the first thing inside <table>. Screen readers announce it when a user reaches the table, so they know what it contains before hearing any numbers.

Header cells are what make a table understandable. When a screen reader user moves to a data cell, the reader announces the matching headers, such as “Morning Express, Departs, 06:00”. Add the scope attribute to make the relationship explicit: scope="col" for column headers and scope="row" for row headers.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Train timetable</title>
  <style>
    table { border-collapse: collapse; font-family: sans-serif; }
    th, td { border: 1px solid #999; padding: 0.4rem 0.8rem; text-align: left; }
    thead th { background: #e8f0fe; }
    caption { font-weight: bold; margin-bottom: 0.5rem; }
  </style>
</head>
<body>
  <table>
    <caption>Trains from Chennai to Bengaluru</caption>
    <tr>
      <th scope="col">Train</th>
      <th scope="col">Departs</th>
      <th scope="col">Arrives</th>
    </tr>
    <tr>
      <th scope="row">Morning Express</th>
      <td>06:00</td>
      <td>11:00</td>
    </tr>
    <tr>
      <th scope="row">Evening Mail</th>
      <td>17:30</td>
      <td>22:45</td>
    </tr>
  </table>
</body>
</html>

Tip: Without any CSS, browsers show tables with no borders at all. The lines in these examples come from the <style> block. The border-collapse: collapse rule merges the double borders between cells into single lines.

Grouping rows: thead, tbody and tfoot

Larger tables benefit from grouping rows into sections:

ElementContains
<thead>The row or rows of column headings
<tbody>The main data rows (you can have more than one)
<tfoot>Summary rows such as totals

These elements do not change much visually by default, but they make the structure clear, give you convenient hooks for CSS, and let browsers repeat header rows on each page when a long table is printed.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Restaurant bill</title>
  <style>
    table { border-collapse: collapse; font-family: sans-serif; }
    th, td { border: 1px solid #999; padding: 0.4rem 0.8rem; text-align: left; }
    thead th { background: #e8f0fe; }
    caption { font-weight: bold; margin-bottom: 0.5rem; }
    .num { text-align: right; }
    tfoot { font-weight: bold; }
  </style>
</head>
<body>
  <table>
    <caption>Lunch bill – Amma Mess</caption>
    <thead>
      <tr>
        <th scope="col">Item</th>
        <th scope="col">Qty</th>
        <th scope="col">Amount (₹)</th>
      </tr>
    </thead>
    <tbody>
      <tr><td>Meals</td><td>2</td><td class="num">240</td></tr>
      <tr><td>Curd rice</td><td>1</td><td class="num">70</td></tr>
      <tr><td>Buttermilk</td><td>2</td><td class="num">40</td></tr>
    </tbody>
    <tfoot>
      <tr><th scope="row" colspan="2">Total</th><td class="num">350</td></tr>
    </tfoot>
  </table>
</body>
</html>

Merging cells with colspan and rowspan

Sometimes a cell needs to stretch across several columns or rows. colspan="2" makes a cell two columns wide; rowspan="2" makes it two rows tall. The total in the bill above uses colspan so that the word “Total” covers the Item and Qty columns.

When a cell spans rows, the rows below it need one fewer cell, because that space is already taken. Count carefully – this is where most table bugs come from.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Timetable</title>
  <style>
    table { border-collapse: collapse; font-family: sans-serif; }
    th, td { border: 1px solid #999; padding: 0.4rem 0.8rem; text-align: left; }
    thead th { background: #e8f0fe; }
    caption { font-weight: bold; margin-bottom: 0.5rem; }
  </style>
</head>
<body>
  <table>
    <caption>Monday timetable</caption>
    <thead>
      <tr><th scope="col">Time</th><th scope="col">Class 8A</th><th scope="col">Class 8B</th></tr>
    </thead>
    <tbody>
      <tr><th scope="row">9:00</th><td rowspan="2">Science lab (double period)</td><td>Maths</td></tr>
      <tr><th scope="row">10:00</th><td>English</td></tr>
      <tr><th scope="row">11:00</th><td colspan="2">Assembly in the main hall</td></tr>
    </tbody>
  </table>
</body>
</html>

Keep merged cells to a minimum. Complex tables with many spans are hard for screen readers to announce and hard for everyone to scan. Often, splitting one complex table into two simple ones is the better design.

Tables are for data, not layout

Years ago, before modern CSS, developers used tables to position headers, sidebars and columns on a page. Do not do this today. Layout tables confuse screen readers (which announce rows and columns that mean nothing), are hard to maintain and do not adapt well to phones. Use CSS Flexbox and CSS Grid for layout instead.

Watch out: If you remove the table borders and the content still makes sense as a grid of comparable values, it is data. If the “table” is only arranging boxes on the page, it is layout – use CSS.

HTML tables on small screens

Wide tables can overflow a phone screen. A simple, reliable fix is to wrap the table in a container that scrolls sideways: <div class="table-wrap"> with the CSS .table-wrap { overflow-x: auto; }. The page stays put while the table scrolls within its box.

Tables often display information that users submit. In the next lesson, HTML Forms, you will learn how to collect that information in the first place.

Further reading

Key takeaways

  • Use tables only for tabular data, never for page layout.
  • Build tables from <table>, <tr>, <th> and <td>, and give each table a <caption>.
  • Use <th> with scope="col" or scope="row" so screen readers can announce headers.
  • Group rows with <thead>, <tbody> and <tfoot>.
  • colspan and rowspan merge cells – use them sparingly and count cells carefully.

Quick quiz

Check what you learned – pick an answer.

1. Which element should hold a column heading?

2. A cell with colspan="3"…

3. Where must the <caption> element go?

Practice exercise

Create a table with the caption "Weekly temperature in Coimbatore" showing three days (Monday, Tuesday, Wednesday) as row headers, with columns for "High (°C)" and "Low (°C)". Use thead and tbody.

Show a hint

The first column cells are row headers: <th scope="row">Monday</th>.

Show the solution
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Weather table</title>
  <style>
    table { border-collapse: collapse; font-family: sans-serif; }
    th, td { border: 1px solid #999; padding: 0.4rem 0.8rem; text-align: left; }
    thead th { background: #e8f0fe; }
    caption { font-weight: bold; margin-bottom: 0.5rem; }
  </style>
</head>
<body>
  <table>
    <caption>Weekly temperature in Coimbatore</caption>
    <thead>
      <tr>
        <th scope="col">Day</th>
        <th scope="col">High (°C)</th>
        <th scope="col">Low (°C)</th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">Monday</th><td>31</td><td>22</td></tr>
      <tr><th scope="row">Tuesday</th><td>32</td><td>23</td></tr>
      <tr><th scope="row">Wednesday</th><td>30</td><td>21</td></tr>
    </tbody>
  </table>
</body>
</html>

Progress is saved in this browser only.